Convert polygerrit to es6-modules

This change replace all HTML imports with es6-modules. The only exceptions are:
* gr-app.html file, which can be deleted only after updating the
  gerrit/httpd/raw/PolyGerritIndexHtml.soy file.
* dark-theme.html which is loaded via importHref. Must be updated manually
  later in a separate change.

This change was produced automatically by ./es6-modules-converter.sh script.
No manual changes were made.

Change-Id: I0c447dd8c05757741e2c940720652d01d9fb7d67
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
index 66c00f9..6d9f9d7 100644
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
@@ -1,6 +1,6 @@
 /**
  * @license
- * Copyright (C) 2016 The Android Open Source Project
+ * 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.
@@ -14,106 +14,118 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-(function() {
-  'use strict';
+import '../../../behaviors/gr-display-name-behavior/gr-display-name-behavior.js';
 
-  const INTERPOLATE_URL_PATTERN = /\$\{([\w]+)\}/g;
+import '../../../scripts/bundled-polymer.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-dropdown/gr-dropdown.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-avatar/gr-avatar.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-account-dropdown_html.js';
 
-  /**
-   * @appliesMixin Gerrit.DisplayNameMixin
-   * @extends Polymer.Element
-   */
-  class GrAccountDropdown extends Polymer.mixinBehaviors( [
-    Gerrit.DisplayNameBehavior,
-  ], Polymer.GestureEventListeners(
-      Polymer.LegacyElementMixin(
-          Polymer.Element))) {
-    static get is() { return 'gr-account-dropdown'; }
+const INTERPOLATE_URL_PATTERN = /\$\{([\w]+)\}/g;
 
-    static get properties() {
-      return {
-        account: Object,
-        config: Object,
-        links: {
-          type: Array,
-          computed: '_getLinks(_switchAccountUrl, _path)',
-        },
-        topContent: {
-          type: Array,
-          computed: '_getTopContent(account)',
-        },
-        _path: {
-          type: String,
-          value: '/',
-        },
-        _hasAvatars: Boolean,
-        _switchAccountUrl: String,
-      };
-    }
+/**
+ * @appliesMixin Gerrit.DisplayNameMixin
+ * @extends Polymer.Element
+ */
+class GrAccountDropdown extends mixinBehaviors( [
+  Gerrit.DisplayNameBehavior,
+], GestureEventListeners(
+    LegacyElementMixin(
+        PolymerElement))) {
+  static get template() { return htmlTemplate; }
 
-    /** @override */
-    attached() {
-      super.attached();
-      this._handleLocationChange();
-      this.listen(window, 'location-change', '_handleLocationChange');
-      this.$.restAPI.getConfig().then(cfg => {
-        this.config = cfg;
+  static get is() { return 'gr-account-dropdown'; }
 
-        if (cfg && cfg.auth && cfg.auth.switch_account_url) {
-          this._switchAccountUrl = cfg.auth.switch_account_url;
-        } else {
-          this._switchAccountUrl = '';
-        }
-        this._hasAvatars = !!(cfg && cfg.plugin && cfg.plugin.has_avatars);
-      });
-    }
-
-    /** @override */
-    detached() {
-      super.detached();
-      this.unlisten(window, 'location-change', '_handleLocationChange');
-    }
-
-    _getLinks(switchAccountUrl, path) {
-      // Polymer 2: check for undefined
-      if ([switchAccountUrl, path].some(arg => arg === undefined)) {
-        return undefined;
-      }
-
-      const links = [{name: 'Settings', url: '/settings/'}];
-      if (switchAccountUrl) {
-        const replacements = {path};
-        const url = this._interpolateUrl(switchAccountUrl, replacements);
-        links.push({name: 'Switch account', url, external: true});
-      }
-      links.push({name: 'Sign out', url: '/logout'});
-      return links;
-    }
-
-    _getTopContent(account) {
-      return [
-        {text: this._accountName(account), bold: true},
-        {text: account.email ? account.email : ''},
-      ];
-    }
-
-    _handleLocationChange() {
-      this._path =
-          window.location.pathname +
-          window.location.search +
-          window.location.hash;
-    }
-
-    _interpolateUrl(url, replacements) {
-      return url.replace(
-          INTERPOLATE_URL_PATTERN,
-          (match, p1) => replacements[p1] || '');
-    }
-
-    _accountName(account) {
-      return this.getUserName(this.config, account, true);
-    }
+  static get properties() {
+    return {
+      account: Object,
+      config: Object,
+      links: {
+        type: Array,
+        computed: '_getLinks(_switchAccountUrl, _path)',
+      },
+      topContent: {
+        type: Array,
+        computed: '_getTopContent(account)',
+      },
+      _path: {
+        type: String,
+        value: '/',
+      },
+      _hasAvatars: Boolean,
+      _switchAccountUrl: String,
+    };
   }
 
-  customElements.define(GrAccountDropdown.is, GrAccountDropdown);
-})();
+  /** @override */
+  attached() {
+    super.attached();
+    this._handleLocationChange();
+    this.listen(window, 'location-change', '_handleLocationChange');
+    this.$.restAPI.getConfig().then(cfg => {
+      this.config = cfg;
+
+      if (cfg && cfg.auth && cfg.auth.switch_account_url) {
+        this._switchAccountUrl = cfg.auth.switch_account_url;
+      } else {
+        this._switchAccountUrl = '';
+      }
+      this._hasAvatars = !!(cfg && cfg.plugin && cfg.plugin.has_avatars);
+    });
+  }
+
+  /** @override */
+  detached() {
+    super.detached();
+    this.unlisten(window, 'location-change', '_handleLocationChange');
+  }
+
+  _getLinks(switchAccountUrl, path) {
+    // Polymer 2: check for undefined
+    if ([switchAccountUrl, path].some(arg => arg === undefined)) {
+      return undefined;
+    }
+
+    const links = [{name: 'Settings', url: '/settings/'}];
+    if (switchAccountUrl) {
+      const replacements = {path};
+      const url = this._interpolateUrl(switchAccountUrl, replacements);
+      links.push({name: 'Switch account', url, external: true});
+    }
+    links.push({name: 'Sign out', url: '/logout'});
+    return links;
+  }
+
+  _getTopContent(account) {
+    return [
+      {text: this._accountName(account), bold: true},
+      {text: account.email ? account.email : ''},
+    ];
+  }
+
+  _handleLocationChange() {
+    this._path =
+        window.location.pathname +
+        window.location.search +
+        window.location.hash;
+  }
+
+  _interpolateUrl(url, replacements) {
+    return url.replace(
+        INTERPOLATE_URL_PATTERN,
+        (match, p1) => replacements[p1] || '');
+  }
+
+  _accountName(account) {
+    return this.getUserName(this.config, account, true);
+  }
+}
+
+customElements.define(GrAccountDropdown.is, GrAccountDropdown);
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_html.js b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_html.js
index 5152ef9..e22db65 100644
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_html.js
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_html.js
@@ -1,30 +1,22 @@
-<!--
-@license
-Copyright (C) 2015 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.
+ */
+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.
--->
-
-<link rel="import" href="../../../behaviors/gr-display-name-behavior/gr-display-name-behavior.html">
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-dropdown/gr-dropdown.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-avatar/gr-avatar.html">
-
-<dom-module id="gr-account-dropdown">
-  <template>
+export const htmlTemplate = html`
     <style include="shared-styles">
       gr-dropdown {
         padding: 0 var(--spacing-m);
@@ -41,16 +33,9 @@
         vertical-align: middle;
       }
     </style>
-    <gr-dropdown
-        link
-        items=[[links]]
-        top-content=[[topContent]]
-        horizontal-align="right">
-        <span hidden$="[[_hasAvatars]]" hidden>[[_accountName(account)]]</span>
-        <gr-avatar account="[[account]]" hidden$="[[!_hasAvatars]]" hidden
-            image-size="56" aria-label="Account avatar"></gr-avatar>
+    <gr-dropdown link="" items="[[links]]" top-content="[[topContent]]" horizontal-align="right">
+        <span hidden\$="[[_hasAvatars]]" hidden="">[[_accountName(account)]]</span>
+        <gr-avatar account="[[account]]" hidden\$="[[!_hasAvatars]]" hidden="" image-size="56" aria-label="Account avatar"></gr-avatar>
     </gr-dropdown>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
-  </template>
-  <script src="gr-account-dropdown.js"></script>
-</dom-module>
+`;
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.html
index 0a11df1..fa0c7a7 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.html
@@ -19,15 +19,20 @@
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-account-dropdown</title>
 
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-account-dropdown.html">
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
+<script type="module" src="../../../test/test-pre-setup.js"></script>
+<script type="module" src="../../../test/common-test-setup.js"></script>
+<script type="module" src="./gr-account-dropdown.js"></script>
 
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-account-dropdown.js';
+void(0);
+</script>
 
 <test-fixture id="basic">
   <template>
@@ -35,93 +40,95 @@
   </template>
 </test-fixture>
 
-<script>
-  suite('gr-account-dropdown tests', async () => {
-    await readyToTest();
-    let element;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-account-dropdown.js';
+suite('gr-account-dropdown tests', () => {
+  let element;
 
-    setup(() => {
-      stub('gr-rest-api-interface', {
-        getConfig() { return Promise.resolve({}); },
-      });
-      element = fixture('basic');
+  setup(() => {
+    stub('gr-rest-api-interface', {
+      getConfig() { return Promise.resolve({}); },
+    });
+    element = fixture('basic');
+  });
+
+  test('account information', () => {
+    element.account = {name: 'John Doe', email: 'john@doe.com'};
+    assert.deepEqual(element.topContent,
+        [{text: 'John Doe', bold: true}, {text: 'john@doe.com'}]);
+  });
+
+  test('test for account without a name', () => {
+    element.account = {id: '0001'};
+    assert.deepEqual(element.topContent,
+        [{text: 'Anonymous', bold: true}, {text: ''}]);
+  });
+
+  test('test for account without a name but using config', () => {
+    element.config = {
+      user: {
+        anonymous_coward_name: 'WikiGerrit',
+      },
+    };
+    element.account = {id: '0001'};
+    assert.deepEqual(element.topContent,
+        [{text: 'WikiGerrit', bold: true}, {text: ''}]);
+  });
+
+  test('test for account name as an email', () => {
+    element.config = {
+      user: {
+        anonymous_coward_name: 'WikiGerrit',
+      },
+    };
+    element.account = {email: 'john@doe.com'};
+    assert.deepEqual(element.topContent,
+        [{text: 'john@doe.com', bold: true}, {text: 'john@doe.com'}]);
+  });
+
+  test('switch account', () => {
+    // Missing params.
+    assert.isUndefined(element._getLinks());
+    assert.isUndefined(element._getLinks(null));
+
+    // No switch account link.
+    assert.equal(element._getLinks(null, '').length, 2);
+
+    // Unparameterized switch account link.
+    let links = element._getLinks('/switch-account', '');
+    assert.equal(links.length, 3);
+    assert.deepEqual(links[1], {
+      name: 'Switch account',
+      url: '/switch-account',
+      external: true,
     });
 
-    test('account information', () => {
-      element.account = {name: 'John Doe', email: 'john@doe.com'};
-      assert.deepEqual(element.topContent,
-          [{text: 'John Doe', bold: true}, {text: 'john@doe.com'}]);
-    });
-
-    test('test for account without a name', () => {
-      element.account = {id: '0001'};
-      assert.deepEqual(element.topContent,
-          [{text: 'Anonymous', bold: true}, {text: ''}]);
-    });
-
-    test('test for account without a name but using config', () => {
-      element.config = {
-        user: {
-          anonymous_coward_name: 'WikiGerrit',
-        },
-      };
-      element.account = {id: '0001'};
-      assert.deepEqual(element.topContent,
-          [{text: 'WikiGerrit', bold: true}, {text: ''}]);
-    });
-
-    test('test for account name as an email', () => {
-      element.config = {
-        user: {
-          anonymous_coward_name: 'WikiGerrit',
-        },
-      };
-      element.account = {email: 'john@doe.com'};
-      assert.deepEqual(element.topContent,
-          [{text: 'john@doe.com', bold: true}, {text: 'john@doe.com'}]);
-    });
-
-    test('switch account', () => {
-      // Missing params.
-      assert.isUndefined(element._getLinks());
-      assert.isUndefined(element._getLinks(null));
-
-      // No switch account link.
-      assert.equal(element._getLinks(null, '').length, 2);
-
-      // Unparameterized switch account link.
-      let links = element._getLinks('/switch-account', '');
-      assert.equal(links.length, 3);
-      assert.deepEqual(links[1], {
-        name: 'Switch account',
-        url: '/switch-account',
-        external: true,
-      });
-
-      // Parameterized switch account link.
-      links = element._getLinks('/switch-account${path}', '/c/123');
-      assert.equal(links.length, 3);
-      assert.deepEqual(links[1], {
-        name: 'Switch account',
-        url: '/switch-account/c/123',
-        external: true,
-      });
-    });
-
-    test('_interpolateUrl', () => {
-      const replacements = {
-        foo: 'bar',
-        test: 'TEST',
-      };
-      const interpolate = function(url) {
-        return element._interpolateUrl(url, replacements);
-      };
-
-      assert.equal(interpolate('test'), 'test');
-      assert.equal(interpolate('${test}'), 'TEST');
-      assert.equal(
-          interpolate('${}, ${test}, ${TEST}, ${foo}'),
-          '${}, TEST, , bar');
+    // Parameterized switch account link.
+    links = element._getLinks('/switch-account${path}', '/c/123');
+    assert.equal(links.length, 3);
+    assert.deepEqual(links[1], {
+      name: 'Switch account',
+      url: '/switch-account/c/123',
+      external: true,
     });
   });
+
+  test('_interpolateUrl', () => {
+    const replacements = {
+      foo: 'bar',
+      test: 'TEST',
+    };
+    const interpolate = function(url) {
+      return element._interpolateUrl(url, replacements);
+    };
+
+    assert.equal(interpolate('test'), 'test');
+    assert.equal(interpolate('${test}'), 'TEST');
+    assert.equal(
+        interpolate('${}, ${test}, ${TEST}, ${foo}'),
+        '${}, TEST, , bar');
+  });
+});
 </script>
diff --git a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.js b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.js
index 63339c9..6814d89 100644
--- a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.js
+++ b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.js
@@ -14,44 +14,51 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-(function() {
-  'use strict';
+import '../../../scripts/bundled-polymer.js';
 
-  /** @extends Polymer.Element */
-  class GrErrorDialog extends Polymer.GestureEventListeners(
-      Polymer.LegacyElementMixin(
-          Polymer.Element)) {
-    static get is() { return 'gr-error-dialog'; }
-    /**
-     * Fired when the dismiss button is pressed.
-     *
-     * @event dismiss
-     */
+import '../../shared/gr-dialog/gr-dialog.js';
+import '../../../styles/shared-styles.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-error-dialog_html.js';
 
-    static get properties() {
-      return {
-        text: String,
-        /**
-         * loginUrl to open on "sign in" button click
-         */
-        loginUrl: {
-          type: String,
-          value: '/login',
-        },
-        /**
-         * Show/hide "Sign In" button in dialog
-         */
-        showSignInButton: {
-          type: Boolean,
-          value: false,
-        },
-      };
-    }
+/** @extends Polymer.Element */
+class GrErrorDialog extends GestureEventListeners(
+    LegacyElementMixin(
+        PolymerElement)) {
+  static get template() { return htmlTemplate; }
 
-    _handleConfirm() {
-      this.dispatchEvent(new CustomEvent('dismiss'));
-    }
+  static get is() { return 'gr-error-dialog'; }
+  /**
+   * Fired when the dismiss button is pressed.
+   *
+   * @event dismiss
+   */
+
+  static get properties() {
+    return {
+      text: String,
+      /**
+       * loginUrl to open on "sign in" button click
+       */
+      loginUrl: {
+        type: String,
+        value: '/login',
+      },
+      /**
+       * Show/hide "Sign In" button in dialog
+       */
+      showSignInButton: {
+        type: Boolean,
+        value: false,
+      },
+    };
   }
 
-  customElements.define(GrErrorDialog.is, GrErrorDialog);
-})();
+  _handleConfirm() {
+    this.dispatchEvent(new CustomEvent('dismiss'));
+  }
+}
+
+customElements.define(GrErrorDialog.is, GrErrorDialog);
diff --git a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_html.js b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_html.js
index ffd7f896..e18d1bd 100644
--- a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_html.js
+++ b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_html.js
@@ -1,26 +1,22 @@
-<!--
-@license
-Copyright (C) 2018 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.
+ */
+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.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-error-dialog">
-  <template>
+export const htmlTemplate = html`
     <style include="shared-styles">
       .main {
         max-height: 40em;
@@ -38,23 +34,11 @@
         text-decoration: none;
       }
     </style>
-    <gr-dialog
-        id="dialog"
-        cancel-label=""
-        on-confirm="_handleConfirm"
-        confirm-label="Dismiss"
-        confirm-on-enter>
+    <gr-dialog id="dialog" cancel-label="" on-confirm="_handleConfirm" confirm-label="Dismiss" confirm-on-enter="">
       <div class="header" slot="header">An error occurred</div>
       <div class="main" slot="main">[[text]]</div>
-      <gr-button
-          id="signIn"
-          class$="signInLink"
-          hidden$="[[!showSignInButton]]"
-          link
-          slot="footer">
-        <a href$="[[loginUrl]]" class="signInLink">Sign in</a>
+      <gr-button id="signIn" class\$="signInLink" hidden\$="[[!showSignInButton]]" link="" slot="footer">
+        <a href\$="[[loginUrl]]" class="signInLink">Sign in</a>
       </gr-button>
     </gr-dialog>
-  </template>
-  <script src="gr-error-dialog.js"></script>
-</dom-module>
+`;
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
index c87f8bb..296c6f0 100644
--- 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
@@ -19,15 +19,20 @@
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-error-dialog</title>
 
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-error-dialog.html">
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
+<script type="module" src="../../../test/test-pre-setup.js"></script>
+<script type="module" src="../../../test/common-test-setup.js"></script>
+<script type="module" src="./gr-error-dialog.js"></script>
 
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-error-dialog.js';
+void(0);
+</script>
 
 <test-fixture id="basic">
   <template>
@@ -35,18 +40,20 @@
   </template>
 </test-fixture>
 
-<script>
-  suite('gr-error-dialog tests', async () => {
-    await readyToTest();
-    let element;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+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);
-    });
+  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-manager/gr-error-manager.js b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
index b828774..e2284a9 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
@@ -14,384 +14,405 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-(function() {
-  'use strict';
+/* Import to get Gerrit interface */
+/* TODO(taoalpha): decouple gr-gerrit from gr-js-api-interface */
+/*
+  FIXME(polymer-modulizer): the above comments were extracted
+  from HTML and may be out of place here. Review them and
+  then delete this comment!
+*/
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
 
-  const HIDE_ALERT_TIMEOUT_MS = 5000;
-  const CHECK_SIGN_IN_INTERVAL_MS = 60 * 1000;
-  const STALE_CREDENTIAL_THRESHOLD_MS = 10 * 60 * 1000;
-  const SIGN_IN_WIDTH_PX = 690;
-  const SIGN_IN_HEIGHT_PX = 500;
-  const TOO_MANY_FILES = 'too many files to find conflicts';
-  const AUTHENTICATION_REQUIRED = 'Authentication required\n';
+import '../../../scripts/bundled-polymer.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../gr-error-dialog/gr-error-dialog.js';
+import '../gr-reporting/gr-reporting.js';
+import '../../shared/gr-alert/gr-alert.js';
+import '../../shared/gr-overlay/gr-overlay.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-error-manager_html.js';
+
+const HIDE_ALERT_TIMEOUT_MS = 5000;
+const CHECK_SIGN_IN_INTERVAL_MS = 60 * 1000;
+const STALE_CREDENTIAL_THRESHOLD_MS = 10 * 60 * 1000;
+const SIGN_IN_WIDTH_PX = 690;
+const SIGN_IN_HEIGHT_PX = 500;
+const TOO_MANY_FILES = 'too many files to find conflicts';
+const AUTHENTICATION_REQUIRED = 'Authentication required\n';
+
+/**
+ * @appliesMixin Gerrit.BaseUrlMixin
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrErrorManager extends mixinBehaviors( [
+  Gerrit.BaseUrlBehavior,
+  Gerrit.FireBehavior,
+], GestureEventListeners(
+    LegacyElementMixin(
+        PolymerElement))) {
+  static get template() { return htmlTemplate; }
+
+  static get is() { return 'gr-error-manager'; }
+
+  static get properties() {
+    return {
+    /**
+     * The ID of the account that was logged in when the app was launched. If
+     * not set, then there was no account at launch.
+     */
+      knownAccountId: Number,
+
+      /** @type {?Object} */
+      _alertElement: Object,
+      /** @type {?number} */
+      _hideAlertHandle: Number,
+      _refreshingCredentials: {
+        type: Boolean,
+        value: false,
+      },
+
+      /**
+       * The time (in milliseconds) since the most recent credential check.
+       */
+      _lastCredentialCheck: {
+        type: Number,
+        value() { return Date.now(); },
+      },
+
+      loginUrl: {
+        type: String,
+        value: '/login',
+      },
+    };
+  }
+
+  constructor() {
+    super();
+
+    /** @type {!Gerrit.Auth} */
+    this._authService = Gerrit.Auth;
+
+    /** @type {?Function} */
+    this._authErrorHandlerDeregistrationHook;
+  }
+
+  /** @override */
+  attached() {
+    super.attached();
+    this.listen(document, 'server-error', '_handleServerError');
+    this.listen(document, 'network-error', '_handleNetworkError');
+    this.listen(document, 'show-alert', '_handleShowAlert');
+    this.listen(document, 'show-error', '_handleShowErrorDialog');
+    this.listen(document, 'visibilitychange', '_handleVisibilityChange');
+    this.listen(document, 'show-auth-required', '_handleAuthRequired');
+
+    this._authErrorHandlerDeregistrationHook = Gerrit.on('auth-error',
+        event => {
+          this._handleAuthError(event.message, event.action);
+        });
+  }
+
+  /** @override */
+  detached() {
+    super.detached();
+    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-error', '_handleShowErrorDialog');
+
+    this._authErrorHandlerDeregistrationHook();
+  }
+
+  _shouldSuppressError(msg) {
+    return msg.includes(TOO_MANY_FILES);
+  }
+
+  _handleAuthRequired() {
+    this._showAuthErrorAlert(
+        'Log in is required to perform that action.', 'Log in.');
+  }
+
+  _handleAuthError(msg, action) {
+    this.$.noInteractionOverlay.open().then(() => {
+      this._showAuthErrorAlert(msg, action);
+    });
+  }
+
+  _handleServerError(e) {
+    const {request, response} = e.detail;
+    response.text().then(errorText => {
+      const url = request && (request.anonymizedUrl || request.url);
+      const {status, statusText} = response;
+      if (response.status === 403
+              && !this._authService.isAuthed
+              && errorText === AUTHENTICATION_REQUIRED) {
+        // if not authed previously, this is trying to access auth required APIs
+        // show auth required alert
+        this._handleAuthRequired();
+      } else if (response.status === 403
+              && this._authService.isAuthed
+              && errorText === AUTHENTICATION_REQUIRED) {
+        // The app was logged at one point and is now getting auth errors.
+        // This indicates the auth token may no longer valid.
+        // Re-check on auth
+        this._authService.clearCache();
+        this.$.restAPI.getLoggedIn();
+      } else if (!this._shouldSuppressError(errorText)) {
+        const trace =
+            response.headers && response.headers.get('X-Gerrit-Trace');
+        if (response.status === 404) {
+          this._showNotFoundMessageWithTip({
+            status,
+            statusText,
+            errorText,
+            url,
+            trace,
+          });
+        } else {
+          this._showErrorDialog(this._constructServerErrorMsg({
+            status,
+            statusText,
+            errorText,
+            url,
+            trace,
+          }));
+        }
+      }
+      console.log(`server error: ${errorText}`);
+    });
+  }
+
+  _showNotFoundMessageWithTip({status, statusText, errorText, url, trace}) {
+    this.$.restAPI.getLoggedIn().then(isLoggedIn => {
+      const tip = isLoggedIn ?
+        'You might have not enough privileges.' :
+        'You might have not enough privileges. Sign in and try again.';
+      this._showErrorDialog(this._constructServerErrorMsg({
+        status,
+        statusText,
+        errorText,
+        url,
+        trace,
+        tip,
+      }), {
+        showSignInButton: !isLoggedIn,
+      });
+    });
+    return;
+  }
+
+  _constructServerErrorMsg({errorText, status, statusText, url, trace, tip}) {
+    let err = '';
+    if (tip) {
+      err += `${tip}\n\n`;
+    }
+    err += `Error ${status}`;
+    if (statusText) { err += ` (${statusText})`; }
+    if (errorText || url) { err += ': '; }
+    if (errorText) { err += errorText; }
+    if (url) { err += `\nEndpoint: ${url}`; }
+    if (trace) { err += `\nTrace Id: ${trace}`; }
+    return err;
+  }
+
+  _handleShowAlert(e) {
+    this._showAlert(e.detail.message, e.detail.action, e.detail.callback,
+        e.detail.dismissOnNavigation);
+  }
+
+  _handleNetworkError(e) {
+    this._showAlert('Server unavailable');
+    console.error(e.detail.error.message);
+  }
 
   /**
-   * @appliesMixin Gerrit.BaseUrlMixin
-   * @appliesMixin Gerrit.FireMixin
-   * @extends Polymer.Element
+   * @param {string} text
+   * @param {?string=} opt_actionText
+   * @param {?Function=} opt_actionCallback
+   * @param {?boolean=} opt_dismissOnNavigation
    */
-  class GrErrorManager extends Polymer.mixinBehaviors( [
-    Gerrit.BaseUrlBehavior,
-    Gerrit.FireBehavior,
-  ], Polymer.GestureEventListeners(
-      Polymer.LegacyElementMixin(
-          Polymer.Element))) {
-    static get is() { return 'gr-error-manager'; }
-
-    static get properties() {
-      return {
-      /**
-       * The ID of the account that was logged in when the app was launched. If
-       * not set, then there was no account at launch.
-       */
-        knownAccountId: Number,
-
-        /** @type {?Object} */
-        _alertElement: Object,
-        /** @type {?number} */
-        _hideAlertHandle: Number,
-        _refreshingCredentials: {
-          type: Boolean,
-          value: false,
-        },
-
-        /**
-         * The time (in milliseconds) since the most recent credential check.
-         */
-        _lastCredentialCheck: {
-          type: Number,
-          value() { return Date.now(); },
-        },
-
-        loginUrl: {
-          type: String,
-          value: '/login',
-        },
-      };
-    }
-
-    constructor() {
-      super();
-
-      /** @type {!Gerrit.Auth} */
-      this._authService = Gerrit.Auth;
-
-      /** @type {?Function} */
-      this._authErrorHandlerDeregistrationHook;
-    }
-
-    /** @override */
-    attached() {
-      super.attached();
-      this.listen(document, 'server-error', '_handleServerError');
-      this.listen(document, 'network-error', '_handleNetworkError');
-      this.listen(document, 'show-alert', '_handleShowAlert');
-      this.listen(document, 'show-error', '_handleShowErrorDialog');
-      this.listen(document, 'visibilitychange', '_handleVisibilityChange');
-      this.listen(document, 'show-auth-required', '_handleAuthRequired');
-
-      this._authErrorHandlerDeregistrationHook = Gerrit.on('auth-error',
-          event => {
-            this._handleAuthError(event.message, event.action);
-          });
-    }
-
-    /** @override */
-    detached() {
-      super.detached();
-      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-error', '_handleShowErrorDialog');
-
-      this._authErrorHandlerDeregistrationHook();
-    }
-
-    _shouldSuppressError(msg) {
-      return msg.includes(TOO_MANY_FILES);
-    }
-
-    _handleAuthRequired() {
-      this._showAuthErrorAlert(
-          'Log in is required to perform that action.', 'Log in.');
-    }
-
-    _handleAuthError(msg, action) {
-      this.$.noInteractionOverlay.open().then(() => {
-        this._showAuthErrorAlert(msg, action);
-      });
-    }
-
-    _handleServerError(e) {
-      const {request, response} = e.detail;
-      response.text().then(errorText => {
-        const url = request && (request.anonymizedUrl || request.url);
-        const {status, statusText} = response;
-        if (response.status === 403
-                && !this._authService.isAuthed
-                && errorText === AUTHENTICATION_REQUIRED) {
-          // if not authed previously, this is trying to access auth required APIs
-          // show auth required alert
-          this._handleAuthRequired();
-        } else if (response.status === 403
-                && this._authService.isAuthed
-                && errorText === AUTHENTICATION_REQUIRED) {
-          // The app was logged at one point and is now getting auth errors.
-          // This indicates the auth token may no longer valid.
-          // Re-check on auth
-          this._authService.clearCache();
-          this.$.restAPI.getLoggedIn();
-        } else if (!this._shouldSuppressError(errorText)) {
-          const trace =
-              response.headers && response.headers.get('X-Gerrit-Trace');
-          if (response.status === 404) {
-            this._showNotFoundMessageWithTip({
-              status,
-              statusText,
-              errorText,
-              url,
-              trace,
-            });
-          } else {
-            this._showErrorDialog(this._constructServerErrorMsg({
-              status,
-              statusText,
-              errorText,
-              url,
-              trace,
-            }));
-          }
-        }
-        console.log(`server error: ${errorText}`);
-      });
-    }
-
-    _showNotFoundMessageWithTip({status, statusText, errorText, url, trace}) {
-      this.$.restAPI.getLoggedIn().then(isLoggedIn => {
-        const tip = isLoggedIn ?
-          'You might have not enough privileges.' :
-          'You might have not enough privileges. Sign in and try again.';
-        this._showErrorDialog(this._constructServerErrorMsg({
-          status,
-          statusText,
-          errorText,
-          url,
-          trace,
-          tip,
-        }), {
-          showSignInButton: !isLoggedIn,
-        });
-      });
-      return;
-    }
-
-    _constructServerErrorMsg({errorText, status, statusText, url, trace, tip}) {
-      let err = '';
-      if (tip) {
-        err += `${tip}\n\n`;
-      }
-      err += `Error ${status}`;
-      if (statusText) { err += ` (${statusText})`; }
-      if (errorText || url) { err += ': '; }
-      if (errorText) { err += errorText; }
-      if (url) { err += `\nEndpoint: ${url}`; }
-      if (trace) { err += `\nTrace Id: ${trace}`; }
-      return err;
-    }
-
-    _handleShowAlert(e) {
-      this._showAlert(e.detail.message, e.detail.action, e.detail.callback,
-          e.detail.dismissOnNavigation);
-    }
-
-    _handleNetworkError(e) {
-      this._showAlert('Server unavailable');
-      console.error(e.detail.error.message);
-    }
-
-    /**
-     * @param {string} text
-     * @param {?string=} opt_actionText
-     * @param {?Function=} opt_actionCallback
-     * @param {?boolean=} opt_dismissOnNavigation
-     */
-    _showAlert(text, opt_actionText, opt_actionCallback,
-        opt_dismissOnNavigation) {
-      if (this._alertElement) {
-        // do not override auth alerts
-        if (this._alertElement.type === 'AUTH') return;
-        this._hideAlert();
-      }
-
-      this._clearHideAlertHandle();
-      if (opt_dismissOnNavigation) {
-        // Persist alert until navigation.
-        this.listen(document, 'location-change', '_hideAlert');
-      } else {
-        this._hideAlertHandle =
-          this.async(this._hideAlert, HIDE_ALERT_TIMEOUT_MS);
-      }
-      const el = this._createToastAlert();
-      el.show(text, opt_actionText, opt_actionCallback);
-      this._alertElement = el;
-    }
-
-    _hideAlert() {
-      if (!this._alertElement) { return; }
-
-      this._alertElement.hide();
-      this._alertElement = null;
-
-      // Remove listener for page navigation, if it exists.
-      this.unlisten(document, 'location-change', '_hideAlert');
-    }
-
-    _clearHideAlertHandle() {
-      if (this._hideAlertHandle != null) {
-        this.cancelAsync(this._hideAlertHandle);
-        this._hideAlertHandle = null;
-      }
-    }
-
-    _showAuthErrorAlert(errorText, actionText) {
-      // hide any existing alert like `reload`
-      // as auth error should have the highest priority
-      if (this._alertElement) {
-        this._alertElement.hide();
-      }
-
-      this._alertElement = this._createToastAlert();
-      this._alertElement.type = 'AUTH';
-      this._alertElement.show(errorText, actionText,
-          this._createLoginPopup.bind(this));
-
-      this._refreshingCredentials = true;
-      this._requestCheckLoggedIn();
-      if (!document.hidden) {
-        this._handleVisibilityChange();
-      }
-    }
-
-    _createToastAlert() {
-      const el = document.createElement('gr-alert');
-      el.toast = true;
-      return el;
-    }
-
-    _handleVisibilityChange() {
-      // Ignore when the page is transitioning to hidden (or hidden is
-      // undefined).
-      if (document.hidden !== false) { return; }
-
-      // If not currently refreshing credentials and the credentials are old,
-      // request them to confirm their validity or (display an auth toast if it
-      // fails).
-      const timeSinceLastCheck = Date.now() - this._lastCredentialCheck;
-      if (!this._refreshingCredentials &&
-          this.knownAccountId !== undefined &&
-          timeSinceLastCheck > STALE_CREDENTIAL_THRESHOLD_MS) {
-        this._lastCredentialCheck = Date.now();
-
-        // check auth status in case:
-        // - user signed out
-        // - user switched account
-        this._checkSignedIn();
-      }
-    }
-
-    _requestCheckLoggedIn() {
-      this.debounce(
-          'checkLoggedIn', this._checkSignedIn, CHECK_SIGN_IN_INTERVAL_MS);
-    }
-
-    _checkSignedIn() {
-      this._lastCredentialCheck = Date.now();
-
-      // force to refetch account info
-      this.$.restAPI.invalidateAccountsCache();
-      this._authService.clearCache();
-
-      this.$.restAPI.getLoggedIn().then(isLoggedIn => {
-        // do nothing if its refreshing
-        if (!this._refreshingCredentials) return;
-
-        if (!isLoggedIn) {
-          // check later
-          // 1. guest mode
-          // 2. or signed out
-          // in case #2, auth-error is taken care of separately
-          this._requestCheckLoggedIn();
-        } else {
-          // check account
-          this.$.restAPI.getAccount().then(account => {
-            if (this._refreshingCredentials) {
-              // If the credentials were refreshed but the account is different
-              // then reload the page completely.
-              if (account._account_id !== this.knownAccountId) {
-                this._reloadPage();
-                return;
-              }
-
-              this._handleCredentialRefreshed();
-            }
-          });
-        }
-      });
-    }
-
-    _reloadPage() {
-      window.location.reload();
-    }
-
-    _createLoginPopup() {
-      const left = window.screenLeft +
-          (window.outerWidth - SIGN_IN_WIDTH_PX) / 2;
-      const top = window.screenTop +
-          (window.outerHeight - SIGN_IN_HEIGHT_PX) / 2;
-      const options = [
-        'width=' + SIGN_IN_WIDTH_PX,
-        'height=' + SIGN_IN_HEIGHT_PX,
-        'left=' + left,
-        'top=' + top,
-      ];
-      window.open(this.getBaseUrl() +
-          '/login/%3FcloseAfterLogin', '_blank', options.join(','));
-      this.listen(window, 'focus', '_handleWindowFocus');
-    }
-
-    _handleCredentialRefreshed() {
-      this.unlisten(window, 'focus', '_handleWindowFocus');
-      this._refreshingCredentials = false;
+  _showAlert(text, opt_actionText, opt_actionCallback,
+      opt_dismissOnNavigation) {
+    if (this._alertElement) {
+      // do not override auth alerts
+      if (this._alertElement.type === 'AUTH') return;
       this._hideAlert();
-      this._showAlert('Credentials refreshed.');
-      this.$.noInteractionOverlay.close();
-
-      // Clear the cache for auth
-      this._authService.clearCache();
     }
 
-    _handleWindowFocus() {
-      this.flushDebouncer('checkLoggedIn');
+    this._clearHideAlertHandle();
+    if (opt_dismissOnNavigation) {
+      // Persist alert until navigation.
+      this.listen(document, 'location-change', '_hideAlert');
+    } else {
+      this._hideAlertHandle =
+        this.async(this._hideAlert, HIDE_ALERT_TIMEOUT_MS);
     }
+    const el = this._createToastAlert();
+    el.show(text, opt_actionText, opt_actionCallback);
+    this._alertElement = el;
+  }
 
-    _handleShowErrorDialog(e) {
-      this._showErrorDialog(e.detail.message);
-    }
+  _hideAlert() {
+    if (!this._alertElement) { return; }
 
-    _handleDismissErrorDialog() {
-      this.$.errorOverlay.close();
-    }
+    this._alertElement.hide();
+    this._alertElement = null;
 
-    _showErrorDialog(message, opt_options) {
-      this.$.reporting.reportErrorDialog(message);
-      this.$.errorDialog.text = message;
-      this.$.errorDialog.showSignInButton =
-          opt_options && opt_options.showSignInButton;
-      this.$.errorOverlay.open();
+    // Remove listener for page navigation, if it exists.
+    this.unlisten(document, 'location-change', '_hideAlert');
+  }
+
+  _clearHideAlertHandle() {
+    if (this._hideAlertHandle != null) {
+      this.cancelAsync(this._hideAlertHandle);
+      this._hideAlertHandle = null;
     }
   }
 
-  customElements.define(GrErrorManager.is, GrErrorManager);
-})();
+  _showAuthErrorAlert(errorText, actionText) {
+    // hide any existing alert like `reload`
+    // as auth error should have the highest priority
+    if (this._alertElement) {
+      this._alertElement.hide();
+    }
+
+    this._alertElement = this._createToastAlert();
+    this._alertElement.type = 'AUTH';
+    this._alertElement.show(errorText, actionText,
+        this._createLoginPopup.bind(this));
+
+    this._refreshingCredentials = true;
+    this._requestCheckLoggedIn();
+    if (!document.hidden) {
+      this._handleVisibilityChange();
+    }
+  }
+
+  _createToastAlert() {
+    const el = document.createElement('gr-alert');
+    el.toast = true;
+    return el;
+  }
+
+  _handleVisibilityChange() {
+    // Ignore when the page is transitioning to hidden (or hidden is
+    // undefined).
+    if (document.hidden !== false) { return; }
+
+    // If not currently refreshing credentials and the credentials are old,
+    // request them to confirm their validity or (display an auth toast if it
+    // fails).
+    const timeSinceLastCheck = Date.now() - this._lastCredentialCheck;
+    if (!this._refreshingCredentials &&
+        this.knownAccountId !== undefined &&
+        timeSinceLastCheck > STALE_CREDENTIAL_THRESHOLD_MS) {
+      this._lastCredentialCheck = Date.now();
+
+      // check auth status in case:
+      // - user signed out
+      // - user switched account
+      this._checkSignedIn();
+    }
+  }
+
+  _requestCheckLoggedIn() {
+    this.debounce(
+        'checkLoggedIn', this._checkSignedIn, CHECK_SIGN_IN_INTERVAL_MS);
+  }
+
+  _checkSignedIn() {
+    this._lastCredentialCheck = Date.now();
+
+    // force to refetch account info
+    this.$.restAPI.invalidateAccountsCache();
+    this._authService.clearCache();
+
+    this.$.restAPI.getLoggedIn().then(isLoggedIn => {
+      // do nothing if its refreshing
+      if (!this._refreshingCredentials) return;
+
+      if (!isLoggedIn) {
+        // check later
+        // 1. guest mode
+        // 2. or signed out
+        // in case #2, auth-error is taken care of separately
+        this._requestCheckLoggedIn();
+      } else {
+        // check account
+        this.$.restAPI.getAccount().then(account => {
+          if (this._refreshingCredentials) {
+            // If the credentials were refreshed but the account is different
+            // then reload the page completely.
+            if (account._account_id !== this.knownAccountId) {
+              this._reloadPage();
+              return;
+            }
+
+            this._handleCredentialRefreshed();
+          }
+        });
+      }
+    });
+  }
+
+  _reloadPage() {
+    window.location.reload();
+  }
+
+  _createLoginPopup() {
+    const left = window.screenLeft +
+        (window.outerWidth - SIGN_IN_WIDTH_PX) / 2;
+    const top = window.screenTop +
+        (window.outerHeight - SIGN_IN_HEIGHT_PX) / 2;
+    const options = [
+      'width=' + SIGN_IN_WIDTH_PX,
+      'height=' + SIGN_IN_HEIGHT_PX,
+      'left=' + left,
+      'top=' + top,
+    ];
+    window.open(this.getBaseUrl() +
+        '/login/%3FcloseAfterLogin', '_blank', options.join(','));
+    this.listen(window, 'focus', '_handleWindowFocus');
+  }
+
+  _handleCredentialRefreshed() {
+    this.unlisten(window, 'focus', '_handleWindowFocus');
+    this._refreshingCredentials = false;
+    this._hideAlert();
+    this._showAlert('Credentials refreshed.');
+    this.$.noInteractionOverlay.close();
+
+    // Clear the cache for auth
+    this._authService.clearCache();
+  }
+
+  _handleWindowFocus() {
+    this.flushDebouncer('checkLoggedIn');
+  }
+
+  _handleShowErrorDialog(e) {
+    this._showErrorDialog(e.detail.message);
+  }
+
+  _handleDismissErrorDialog() {
+    this.$.errorOverlay.close();
+  }
+
+  _showErrorDialog(message, opt_options) {
+    this.$.reporting.reportErrorDialog(message);
+    this.$.errorDialog.text = message;
+    this.$.errorDialog.showSignInButton =
+        opt_options && opt_options.showSignInButton;
+    this.$.errorOverlay.open();
+  }
+}
+
+customElements.define(GrErrorManager.is, GrErrorManager);
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_html.js b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_html.js
index 104d5b0..5661d1e 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_html.js
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_html.js
@@ -1,52 +1,27 @@
-<!--
-@license
-Copyright (C) 2016 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.
+ */
+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.
--->
-
-<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../core/gr-error-dialog/gr-error-dialog.html">
-<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
-<link rel="import" href="../../shared/gr-alert/gr-alert.html">
-<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<!-- Import to get Gerrit interface -->
-<!-- TODO(taoalpha): decouple gr-gerrit from gr-js-api-interface -->
-<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
-
-<dom-module id="gr-error-manager">
-  <template>
-    <gr-overlay with-backdrop id="errorOverlay">
-      <gr-error-dialog
-          id="errorDialog"
-          on-dismiss="_handleDismissErrorDialog"
-          confirm-label="Dismiss"
-          confirm-on-enter
-          login-url="[[loginUrl]]"
-      ></gr-error-dialog>
+export const htmlTemplate = html`
+    <gr-overlay with-backdrop="" id="errorOverlay">
+      <gr-error-dialog id="errorDialog" on-dismiss="_handleDismissErrorDialog" confirm-label="Dismiss" confirm-on-enter="" login-url="[[loginUrl]]"></gr-error-dialog>
     </gr-overlay>
-    <gr-overlay
-      id="noInteractionOverlay"
-      with-backdrop
-      always-on-top
-      no-cancel-on-esc-key
-      no-cancel-on-outside-click>
+    <gr-overlay id="noInteractionOverlay" with-backdrop="" always-on-top="" no-cancel-on-esc-key="" no-cancel-on-outside-click="">
     </gr-overlay>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
     <gr-reporting id="reporting"></gr-reporting>
-  </template>
-  <script src="gr-error-manager.js"></script>
-</dom-module>
+`;
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.html
index e984577..a4a9a7d 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.html
@@ -19,14 +19,18 @@
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-error-manager</title>
 
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html" />
-<link rel="import" href="gr-error-manager.html">
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
+<script type="module" src="../../../test/common-test-setup.js"></script>
+<script type="module" src="./gr-error-manager.js"></script>
 
-<script>void (0);</script>
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-error-manager.js';
+void (0);
+</script>
 
 <test-fixture id="basic">
   <template>
@@ -34,465 +38,467 @@
   </template>
 </test-fixture>
 
-<script>
-  suite('gr-error-manager tests', async () => {
-    await readyToTest();
-    let element;
-    let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-error-manager.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-error-manager tests', () => {
+  let element;
+  let sandbox;
 
+  setup(() => {
+    sandbox = sinon.sandbox.create();
+  });
+
+  teardown(() => {
+    sandbox.restore();
+  });
+
+  suite('when authed', () => {
     setup(() => {
-      sandbox = sinon.sandbox.create();
+      sandbox.stub(window, 'fetch')
+          .returns(Promise.resolve({ok: true, status: 204}));
+      element = fixture('basic');
+      element._authService.clearCache();
     });
 
-    teardown(() => {
-      sandbox.restore();
+    test('does not show auth error on 403 by default', done => {
+      const showAuthErrorStub = sandbox.stub(element, '_showAuthErrorAlert');
+      const responseText = Promise.resolve('server says no.');
+      element.fire('server-error',
+          {response: {status: 403, text() { return responseText; }}}
+      );
+      flush(() => {
+        assert.isFalse(showAuthErrorStub.calledOnce);
+        done();
+      });
     });
 
-    suite('when authed', () => {
-      setup(() => {
-        sandbox.stub(window, 'fetch')
-            .returns(Promise.resolve({ok: true, status: 204}));
-        element = fixture('basic');
-        element._authService.clearCache();
-      });
-
-      test('does not show auth error on 403 by default', done => {
-        const showAuthErrorStub = sandbox.stub(element, '_showAuthErrorAlert');
-        const responseText = Promise.resolve('server says no.');
-        element.fire('server-error',
-            {response: {status: 403, text() { return responseText; }}}
-        );
-        flush(() => {
-          assert.isFalse(showAuthErrorStub.calledOnce);
-          done();
-        });
-      });
-
-      test('show auth required for 403 with auth error and not authed before',
-          done => {
-            const showAuthErrorStub = sandbox.stub(
-                element, '_showAuthErrorAlert'
-            );
-            const responseText = Promise.resolve('Authentication required\n');
-            sinon.stub(element.$.restAPI, 'getLoggedIn')
-                .returns(Promise.resolve(true));
-            element.fire('server-error',
-                {response: {status: 403, text() { return responseText; }}}
-            );
-            flush(() => {
-              assert.isTrue(showAuthErrorStub.calledOnce);
-              done();
-            });
-          });
-
-      test('recheck auth for 403 with auth error if authed before', done => {
-        // starts with authed state
-        element.$.restAPI.getLoggedIn();
-        const responseText = Promise.resolve('Authentication required\n');
-        sinon.stub(element.$.restAPI, 'getLoggedIn')
-            .returns(Promise.resolve(true));
-        element.fire('server-error',
-            {response: {status: 403, text() { return responseText; }}}
-        );
-        flush(() => {
-          assert.isTrue(element.$.restAPI.getLoggedIn.calledOnce);
-          done();
-        });
-      });
-
-      test('show logged in error', () => {
-        sandbox.stub(element, '_showAuthErrorAlert');
-        element.fire('show-auth-required');
-        assert.isTrue(element._showAuthErrorAlert.calledWithExactly(
-            'Log in is required to perform that action.', 'Log in.'));
-      });
-
-      test('show normal Error', done => {
-        const showErrorStub = sandbox.stub(element, '_showErrorDialog');
-        const textSpy = sandbox.spy(() => Promise.resolve('ZOMG'));
-        element.fire('server-error', {response: {status: 500, text: textSpy}});
-
-        assert.isTrue(textSpy.called);
-        flush(() => {
-          assert.isTrue(showErrorStub.calledOnce);
-          assert.isTrue(showErrorStub.lastCall.calledWithExactly(
-              'Error 500: ZOMG'));
-          done();
-        });
-      });
-
-      test('_constructServerErrorMsg', () => {
-        const errorText = 'change conflicts';
-        const status = 409;
-        const statusText = 'Conflict';
-        const url = '/my/test/url';
-
-        assert.equal(element._constructServerErrorMsg({status}),
-            'Error 409');
-        assert.equal(element._constructServerErrorMsg({status, url}),
-            'Error 409: \nEndpoint: /my/test/url');
-        assert.equal(element.
-            _constructServerErrorMsg({status, statusText, url}),
-        'Error 409 (Conflict): \nEndpoint: /my/test/url');
-        assert.equal(element._constructServerErrorMsg({
-          status,
-          statusText,
-          errorText,
-          url,
-        }), 'Error 409 (Conflict): change conflicts' +
-        '\nEndpoint: /my/test/url');
-        assert.equal(element._constructServerErrorMsg({
-          status,
-          statusText,
-          errorText,
-          url,
-          trace: 'xxxxx',
-        }), 'Error 409 (Conflict): change conflicts' +
-        '\nEndpoint: /my/test/url\nTrace Id: xxxxx');
-      });
-
-      test('extract trace id from headers if exists', done => {
-        const textSpy = sandbox.spy(
-            () => Promise.resolve('500')
-        );
-        const headers = new Headers();
-        headers.set('X-Gerrit-Trace', 'xxxx');
-        element.fire('server-error', {
-          response: {
-            headers,
-            status: 500,
-            text: textSpy,
-          },
-        });
-        flush(() => {
-          assert.equal(
-              element.$.errorDialog.text,
-              'Error 500: 500\nTrace Id: xxxx'
+    test('show auth required for 403 with auth error and not authed before',
+        done => {
+          const showAuthErrorStub = sandbox.stub(
+              element, '_showAuthErrorAlert'
           );
-          done();
-        });
-      });
-
-      test('suppress TOO_MANY_FILES error', done => {
-        const showAlertStub = sandbox.stub(element, '_showAlert');
-        const textSpy = sandbox.spy(
-            () => Promise.resolve('too many files to find conflicts')
-        );
-        element.fire('server-error', {response: {status: 500, text: textSpy}});
-
-        assert.isTrue(textSpy.called);
-        flush(() => {
-          assert.isFalse(showAlertStub.called);
-          done();
-        });
-      });
-
-      test('show network error', done => {
-        const consoleErrorStub = sandbox.stub(console, 'error');
-        const showAlertStub = sandbox.stub(element, '_showAlert');
-        element.fire('network-error', {error: new Error('ZOMG')});
-        flush(() => {
-          assert.isTrue(showAlertStub.calledOnce);
-          assert.isTrue(showAlertStub.lastCall.calledWithExactly(
-              'Server unavailable'));
-          assert.isTrue(consoleErrorStub.calledOnce);
-          assert.isTrue(consoleErrorStub.lastCall.calledWithExactly('ZOMG'));
-          done();
-        });
-      });
-
-      test('show auth refresh toast', done => {
-        // starts with authed state
-        element.$.restAPI.getLoggedIn();
-        const refreshStub = sandbox.stub(element.$.restAPI, 'getAccount',
-            () => Promise.resolve({}));
-        const toastSpy = sandbox.spy(element, '_createToastAlert');
-        const windowOpen = sandbox.stub(window, 'open');
-        const responseText = Promise.resolve('Authentication required\n');
-        // fake failed auth
-        window.fetch.returns(Promise.resolve({status: 403}));
-        element.fire('server-error',
-            {response: {status: 403, text() { return responseText; }}}
-        );
-        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);
+          const responseText = Promise.resolve('Authentication required\n');
+          sinon.stub(element.$.restAPI, 'getLoggedIn')
+              .returns(Promise.resolve(true));
+          element.fire('server-error',
+              {response: {status: 403, text() { return responseText; }}}
+          );
           flush(() => {
-            // auth-error fired
-            assert.isTrue(toastSpy.called);
+            assert.isTrue(showAuthErrorStub.calledOnce);
+            done();
+          });
+        });
 
-            // toast
-            let toast = toastSpy.lastCall.returnValue;
+    test('recheck auth for 403 with auth error if authed before', done => {
+      // starts with authed state
+      element.$.restAPI.getLoggedIn();
+      const responseText = Promise.resolve('Authentication required\n');
+      sinon.stub(element.$.restAPI, 'getLoggedIn')
+          .returns(Promise.resolve(true));
+      element.fire('server-error',
+          {response: {status: 403, text() { return responseText; }}}
+      );
+      flush(() => {
+        assert.isTrue(element.$.restAPI.getLoggedIn.calledOnce);
+        done();
+      });
+    });
+
+    test('show logged in error', () => {
+      sandbox.stub(element, '_showAuthErrorAlert');
+      element.fire('show-auth-required');
+      assert.isTrue(element._showAuthErrorAlert.calledWithExactly(
+          'Log in is required to perform that action.', 'Log in.'));
+    });
+
+    test('show normal Error', done => {
+      const showErrorStub = sandbox.stub(element, '_showErrorDialog');
+      const textSpy = sandbox.spy(() => Promise.resolve('ZOMG'));
+      element.fire('server-error', {response: {status: 500, text: textSpy}});
+
+      assert.isTrue(textSpy.called);
+      flush(() => {
+        assert.isTrue(showErrorStub.calledOnce);
+        assert.isTrue(showErrorStub.lastCall.calledWithExactly(
+            'Error 500: ZOMG'));
+        done();
+      });
+    });
+
+    test('_constructServerErrorMsg', () => {
+      const errorText = 'change conflicts';
+      const status = 409;
+      const statusText = 'Conflict';
+      const url = '/my/test/url';
+
+      assert.equal(element._constructServerErrorMsg({status}),
+          'Error 409');
+      assert.equal(element._constructServerErrorMsg({status, url}),
+          'Error 409: \nEndpoint: /my/test/url');
+      assert.equal(element.
+          _constructServerErrorMsg({status, statusText, url}),
+      'Error 409 (Conflict): \nEndpoint: /my/test/url');
+      assert.equal(element._constructServerErrorMsg({
+        status,
+        statusText,
+        errorText,
+        url,
+      }), 'Error 409 (Conflict): change conflicts' +
+      '\nEndpoint: /my/test/url');
+      assert.equal(element._constructServerErrorMsg({
+        status,
+        statusText,
+        errorText,
+        url,
+        trace: 'xxxxx',
+      }), 'Error 409 (Conflict): change conflicts' +
+      '\nEndpoint: /my/test/url\nTrace Id: xxxxx');
+    });
+
+    test('extract trace id from headers if exists', done => {
+      const textSpy = sandbox.spy(
+          () => Promise.resolve('500')
+      );
+      const headers = new Headers();
+      headers.set('X-Gerrit-Trace', 'xxxx');
+      element.fire('server-error', {
+        response: {
+          headers,
+          status: 500,
+          text: textSpy,
+        },
+      });
+      flush(() => {
+        assert.equal(
+            element.$.errorDialog.text,
+            'Error 500: 500\nTrace Id: xxxx'
+        );
+        done();
+      });
+    });
+
+    test('suppress TOO_MANY_FILES error', done => {
+      const showAlertStub = sandbox.stub(element, '_showAlert');
+      const textSpy = sandbox.spy(
+          () => Promise.resolve('too many files to find conflicts')
+      );
+      element.fire('server-error', {response: {status: 500, text: textSpy}});
+
+      assert.isTrue(textSpy.called);
+      flush(() => {
+        assert.isFalse(showAlertStub.called);
+        done();
+      });
+    });
+
+    test('show network error', done => {
+      const consoleErrorStub = sandbox.stub(console, 'error');
+      const showAlertStub = sandbox.stub(element, '_showAlert');
+      element.fire('network-error', {error: new Error('ZOMG')});
+      flush(() => {
+        assert.isTrue(showAlertStub.calledOnce);
+        assert.isTrue(showAlertStub.lastCall.calledWithExactly(
+            'Server unavailable'));
+        assert.isTrue(consoleErrorStub.calledOnce);
+        assert.isTrue(consoleErrorStub.lastCall.calledWithExactly('ZOMG'));
+        done();
+      });
+    });
+
+    test('show auth refresh toast', done => {
+      // starts with authed state
+      element.$.restAPI.getLoggedIn();
+      const refreshStub = sandbox.stub(element.$.restAPI, 'getAccount',
+          () => Promise.resolve({}));
+      const toastSpy = sandbox.spy(element, '_createToastAlert');
+      const windowOpen = sandbox.stub(window, 'open');
+      const responseText = Promise.resolve('Authentication required\n');
+      // fake failed auth
+      window.fetch.returns(Promise.resolve({status: 403}));
+      element.fire('server-error',
+          {response: {status: 403, text() { return responseText; }}}
+      );
+      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);
+
+          // 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');
+
+          // 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);
+
+          // @see Issue 5822: noopener breaks closeAfterLogin
+          assert.equal(windowOpen.lastCall.args[2].indexOf('noopener=yes'),
+              -1);
+
+          const hideToastSpy = sandbox.spy(toast, 'hide');
+
+          // now fake authed
+          window.fetch.returns(Promise.resolve({status: 204}));
+          element._handleWindowFocus();
+          element.flushDebouncer('checkLoggedIn');
+          flush(() => {
+            assert.isTrue(refreshStub.called);
+            assert.isTrue(hideToastSpy.called);
+
+            // toast update
+            assert.notStrictEqual(toastSpy.lastCall.returnValue, toast);
+            toast = toastSpy.lastCall.returnValue;
             assert.isOk(toast);
             assert.include(
-                Polymer.dom(toast.root).textContent, 'Credentials expired.');
-            assert.include(
-                Polymer.dom(toast.root).textContent, 'Refresh credentials');
+                dom(toast.root).textContent, 'Credentials refreshed');
 
-            // 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);
-
-            // @see Issue 5822: noopener breaks closeAfterLogin
-            assert.equal(windowOpen.lastCall.args[2].indexOf('noopener=yes'),
-                -1);
-
-            const hideToastSpy = sandbox.spy(toast, 'hide');
-
-            // now fake authed
-            window.fetch.returns(Promise.resolve({status: 204}));
-            element._handleWindowFocus();
-            element.flushDebouncer('checkLoggedIn');
-            flush(() => {
-              assert.isTrue(refreshStub.called);
-              assert.isTrue(hideToastSpy.called);
-
-              // toast update
-              assert.notStrictEqual(toastSpy.lastCall.returnValue, toast);
-              toast = toastSpy.lastCall.returnValue;
-              assert.isOk(toast);
-              assert.include(
-                  Polymer.dom(toast.root).textContent, 'Credentials refreshed');
-
-              // close overlay
-              assert.isTrue(noInteractionOverlay.close.called);
-              done();
-            });
-          });
-        });
-      });
-
-      test('auth toast should dismiss existing toast', done => {
-        // starts with authed state
-        element.$.restAPI.getLoggedIn();
-        const toastSpy = sandbox.spy(element, '_createToastAlert');
-        const responseText = Promise.resolve('Authentication required\n');
-
-        // fake an alert
-        element.fire('show-alert', {message: 'test reload', action: 'reload'});
-        const toast = toastSpy.lastCall.returnValue;
-        assert.isOk(toast);
-        assert.include(
-            Polymer.dom(toast.root).textContent, 'test reload');
-
-        // fake auth
-        window.fetch.returns(Promise.resolve({status: 403}));
-        element.fire('server-error',
-            {response: {status: 403, text() { return responseText; }}}
-        );
-        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(() => {
-            // toast
-            const toast = toastSpy.lastCall.returnValue;
-            assert.include(
-                Polymer.dom(toast.root).textContent, 'Credentials expired.');
-            assert.include(
-                Polymer.dom(toast.root).textContent, 'Refresh credentials');
+            // close overlay
+            assert.isTrue(noInteractionOverlay.close.called);
             done();
           });
         });
       });
+    });
 
-      test('regular toast should dismiss regular toast', () => {
-        // starts with authed state
-        element.$.restAPI.getLoggedIn();
-        const toastSpy = sandbox.spy(element, '_createToastAlert');
+    test('auth toast should dismiss existing toast', done => {
+      // starts with authed state
+      element.$.restAPI.getLoggedIn();
+      const toastSpy = sandbox.spy(element, '_createToastAlert');
+      const responseText = Promise.resolve('Authentication required\n');
 
-        // fake an alert
-        element.fire('show-alert', {message: 'test reload', action: 'reload'});
-        let toast = toastSpy.lastCall.returnValue;
-        assert.isOk(toast);
-        assert.include(
-            Polymer.dom(toast.root).textContent, 'test reload');
+      // fake an alert
+      element.fire('show-alert', {message: 'test reload', action: 'reload'});
+      const toast = toastSpy.lastCall.returnValue;
+      assert.isOk(toast);
+      assert.include(
+          dom(toast.root).textContent, 'test reload');
 
-        // new alert
-        element.fire('show-alert', {message: 'second-test', action: 'reload'});
-
-        toast = toastSpy.lastCall.returnValue;
-        assert.include(Polymer.dom(toast.root).textContent, 'second-test');
-      });
-
-      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
-        window.fetch.returns(Promise.resolve({status: 403}));
-        element.fire('server-error',
-            {response: {status: 403, text() { return responseText; }}}
-        );
-        assert.equal(window.fetch.callCount, 1);
+      // fake auth
+      window.fetch.returns(Promise.resolve({status: 403}));
+      element.fire('server-error',
+          {response: {status: 403, text() { return responseText; }}}
+      );
+      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(() => {
-          // 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(() => {
-            let toast = toastSpy.lastCall.returnValue;
-            assert.include(
-                Polymer.dom(toast.root).textContent, 'Credentials expired.');
-            assert.include(
-                Polymer.dom(toast.root).textContent, 'Refresh credentials');
+          // toast
+          const toast = toastSpy.lastCall.returnValue;
+          assert.include(
+              dom(toast.root).textContent, 'Credentials expired.');
+          assert.include(
+              dom(toast.root).textContent, 'Refresh credentials');
+          done();
+        });
+      });
+    });
 
-            // fake an alert
-            element.fire('show-alert', {
-              message: 'test-alert', action: 'reload',
-            });
-            flush(() => {
-              toast = toastSpy.lastCall.returnValue;
-              assert.isOk(toast);
-              assert.include(
-                  Polymer.dom(toast.root).textContent, 'Credentials expired.');
-              done();
-            });
+    test('regular toast should dismiss regular toast', () => {
+      // starts with authed state
+      element.$.restAPI.getLoggedIn();
+      const toastSpy = sandbox.spy(element, '_createToastAlert');
+
+      // fake an alert
+      element.fire('show-alert', {message: 'test reload', action: 'reload'});
+      let toast = toastSpy.lastCall.returnValue;
+      assert.isOk(toast);
+      assert.include(
+          dom(toast.root).textContent, 'test reload');
+
+      // new alert
+      element.fire('show-alert', {message: 'second-test', action: 'reload'});
+
+      toast = toastSpy.lastCall.returnValue;
+      assert.include(dom(toast.root).textContent, 'second-test');
+    });
+
+    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
+      window.fetch.returns(Promise.resolve({status: 403}));
+      element.fire('server-error',
+          {response: {status: 403, text() { return responseText; }}}
+      );
+      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(() => {
+          let toast = toastSpy.lastCall.returnValue;
+          assert.include(
+              dom(toast.root).textContent, 'Credentials expired.');
+          assert.include(
+              dom(toast.root).textContent, 'Refresh credentials');
+
+          // fake an alert
+          element.fire('show-alert', {
+            message: 'test-alert', action: 'reload',
+          });
+          flush(() => {
+            toast = toastSpy.lastCall.returnValue;
+            assert.isOk(toast);
+            assert.include(
+                dom(toast.root).textContent, 'Credentials expired.');
+            done();
           });
         });
       });
+    });
 
-      test('show alert', () => {
-        const alertObj = {message: 'foo'};
-        sandbox.stub(element, '_showAlert');
-        element.fire('show-alert', alertObj);
-        assert.isTrue(element._showAlert.calledOnce);
-        assert.equal(element._showAlert.lastCall.args[0], 'foo');
-        assert.isNotOk(element._showAlert.lastCall.args[1]);
-        assert.isNotOk(element._showAlert.lastCall.args[2]);
-      });
+    test('show alert', () => {
+      const alertObj = {message: 'foo'};
+      sandbox.stub(element, '_showAlert');
+      element.fire('show-alert', alertObj);
+      assert.isTrue(element._showAlert.calledOnce);
+      assert.equal(element._showAlert.lastCall.args[0], 'foo');
+      assert.isNotOk(element._showAlert.lastCall.args[1]);
+      assert.isNotOk(element._showAlert.lastCall.args[2]);
+    });
 
-      test('checks stale credentials on visibility change', () => {
-        const refreshStub = sandbox.stub(element,
-            '_checkSignedIn');
-        sandbox.stub(Date, 'now').returns(999999);
-        element._lastCredentialCheck = 0;
-        element._handleVisibilityChange();
+    test('checks stale credentials on visibility change', () => {
+      const refreshStub = sandbox.stub(element,
+          '_checkSignedIn');
+      sandbox.stub(Date, 'now').returns(999999);
+      element._lastCredentialCheck = 0;
+      element._handleVisibilityChange();
 
-        // Since there is no known account, it should not test credentials.
-        assert.isFalse(refreshStub.called);
-        assert.equal(element._lastCredentialCheck, 0);
+      // Since there is no known account, it should not test credentials.
+      assert.isFalse(refreshStub.called);
+      assert.equal(element._lastCredentialCheck, 0);
 
-        element.knownAccountId = 123;
-        element._handleVisibilityChange();
+      element.knownAccountId = 123;
+      element._handleVisibilityChange();
 
-        // Should test credentials, since there is a known account.
-        assert.isTrue(refreshStub.called);
-        assert.equal(element._lastCredentialCheck, 999999);
-      });
+      // Should test credentials, since there is a known account.
+      assert.isTrue(refreshStub.called);
+      assert.equal(element._lastCredentialCheck, 999999);
+    });
 
-      test('refreshes with same credentials', done => {
-        const accountPromise = Promise.resolve({_account_id: 1234});
-        sandbox.stub(element.$.restAPI, 'getAccount')
-            .returns(accountPromise);
-        const requestCheckStub = sandbox.stub(element, '_requestCheckLoggedIn');
-        const handleRefreshStub = sandbox.stub(element,
-            '_handleCredentialRefreshed');
-        const reloadStub = sandbox.stub(element, '_reloadPage');
+    test('refreshes with same credentials', done => {
+      const accountPromise = Promise.resolve({_account_id: 1234});
+      sandbox.stub(element.$.restAPI, 'getAccount')
+          .returns(accountPromise);
+      const requestCheckStub = sandbox.stub(element, '_requestCheckLoggedIn');
+      const handleRefreshStub = sandbox.stub(element,
+          '_handleCredentialRefreshed');
+      const reloadStub = sandbox.stub(element, '_reloadPage');
 
-        element.knownAccountId = 1234;
-        element._refreshingCredentials = true;
-        element._checkSignedIn();
+      element.knownAccountId = 1234;
+      element._refreshingCredentials = true;
+      element._checkSignedIn();
 
-        flush(() => {
-          assert.isFalse(requestCheckStub.called);
-          assert.isTrue(handleRefreshStub.called);
-          assert.isFalse(reloadStub.called);
-          done();
-        });
-      });
-
-      test('_showAlert hides existing alerts', () => {
-        element._alertElement = element._createToastAlert();
-        const hideStub = sandbox.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(
-            element.$.reporting,
-            'reportErrorDialog'
-        );
-
-        const message = 'test message';
-        element.fire('show-error', {message});
-        flushAsynchronousOperations();
-
-        assert.isTrue(openStub.called);
-        assert.isTrue(reportStub.called);
-        assert.equal(element.$.errorDialog.text, message);
-
-        element.$.errorDialog.fire('dismiss');
-        flushAsynchronousOperations();
-
-        assert.isTrue(closeStub.called);
-      });
-
-      test('reloads when refreshed credentials differ', done => {
-        const accountPromise = Promise.resolve({_account_id: 1234});
-        sandbox.stub(element.$.restAPI, 'getAccount')
-            .returns(accountPromise);
-        const requestCheckStub = sandbox.stub(
-            element,
-            '_requestCheckLoggedIn');
-        const handleRefreshStub = sandbox.stub(element,
-            '_handleCredentialRefreshed');
-        const reloadStub = sandbox.stub(element, '_reloadPage');
-
-        element.knownAccountId = 4321; // Different from 1234
-        element._refreshingCredentials = true;
-        element._checkSignedIn();
-
-        flush(() => {
-          assert.isFalse(requestCheckStub.called);
-          assert.isFalse(handleRefreshStub.called);
-          assert.isTrue(reloadStub.called);
-          done();
-        });
+      flush(() => {
+        assert.isFalse(requestCheckStub.called);
+        assert.isTrue(handleRefreshStub.called);
+        assert.isFalse(reloadStub.called);
+        done();
       });
     });
 
-    suite('when not authed', () => {
-      setup(() => {
-        stub('gr-rest-api-interface', {
-          getLoggedIn() { return Promise.resolve(false); },
-        });
-        element = fixture('basic');
-      });
+    test('_showAlert hides existing alerts', () => {
+      element._alertElement = element._createToastAlert();
+      const hideStub = sandbox.stub(element, '_hideAlert');
+      element._showAlert();
+      assert.isTrue(hideStub.calledOnce);
+    });
 
-      test('refresh loop continues on credential fail', done => {
-        const requestCheckStub = sandbox.stub(
-            element,
-            '_requestCheckLoggedIn');
-        const handleRefreshStub = sandbox.stub(element,
-            '_handleCredentialRefreshed');
-        const reloadStub = sandbox.stub(element, '_reloadPage');
+    test('show-error', () => {
+      const openStub = sandbox.stub(element.$.errorOverlay, 'open');
+      const closeStub = sandbox.stub(element.$.errorOverlay, 'close');
+      const reportStub = sandbox.stub(
+          element.$.reporting,
+          'reportErrorDialog'
+      );
 
-        element._refreshingCredentials = true;
-        element._checkSignedIn();
+      const message = 'test message';
+      element.fire('show-error', {message});
+      flushAsynchronousOperations();
 
-        flush(() => {
-          assert.isTrue(requestCheckStub.called);
-          assert.isFalse(handleRefreshStub.called);
-          assert.isFalse(reloadStub.called);
-          done();
-        });
+      assert.isTrue(openStub.called);
+      assert.isTrue(reportStub.called);
+      assert.equal(element.$.errorDialog.text, message);
+
+      element.$.errorDialog.fire('dismiss');
+      flushAsynchronousOperations();
+
+      assert.isTrue(closeStub.called);
+    });
+
+    test('reloads when refreshed credentials differ', done => {
+      const accountPromise = Promise.resolve({_account_id: 1234});
+      sandbox.stub(element.$.restAPI, 'getAccount')
+          .returns(accountPromise);
+      const requestCheckStub = sandbox.stub(
+          element,
+          '_requestCheckLoggedIn');
+      const handleRefreshStub = sandbox.stub(element,
+          '_handleCredentialRefreshed');
+      const reloadStub = sandbox.stub(element, '_reloadPage');
+
+      element.knownAccountId = 4321; // Different from 1234
+      element._refreshingCredentials = true;
+      element._checkSignedIn();
+
+      flush(() => {
+        assert.isFalse(requestCheckStub.called);
+        assert.isFalse(handleRefreshStub.called);
+        assert.isTrue(reloadStub.called);
+        done();
       });
     });
   });
+
+  suite('when not authed', () => {
+    setup(() => {
+      stub('gr-rest-api-interface', {
+        getLoggedIn() { return Promise.resolve(false); },
+      });
+      element = fixture('basic');
+    });
+
+    test('refresh loop continues on credential fail', done => {
+      const requestCheckStub = sandbox.stub(
+          element,
+          '_requestCheckLoggedIn');
+      const handleRefreshStub = sandbox.stub(element,
+          '_handleCredentialRefreshed');
+      const reloadStub = sandbox.stub(element, '_reloadPage');
+
+      element._refreshingCredentials = true;
+      element._checkSignedIn();
+
+      flush(() => {
+        assert.isTrue(requestCheckStub.called);
+        assert.isFalse(handleRefreshStub.called);
+        assert.isFalse(reloadStub.called);
+        done();
+      });
+    });
+  });
+});
 </script>
diff --git a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.js b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.js
index 3d424bc..5d7ec27 100644
--- a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.js
+++ b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.js
@@ -14,30 +14,36 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-(function() {
-  'use strict';
+import '../../../scripts/bundled-polymer.js';
 
-  /** @extends Polymer.Element */
-  class GrKeyBindingDisplay extends Polymer.GestureEventListeners(
-      Polymer.LegacyElementMixin(
-          Polymer.Element)) {
-    static get is() { return 'gr-key-binding-display'; }
+import '../../../styles/shared-styles.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-key-binding-display_html.js';
 
-    static get properties() {
-      return {
-      /** @type {Array<string>} */
-        binding: Array,
-      };
-    }
+/** @extends Polymer.Element */
+class GrKeyBindingDisplay extends GestureEventListeners(
+    LegacyElementMixin(
+        PolymerElement)) {
+  static get template() { return htmlTemplate; }
 
-    _computeModifiers(binding) {
-      return binding.slice(0, binding.length - 1);
-    }
+  static get is() { return 'gr-key-binding-display'; }
 
-    _computeKey(binding) {
-      return binding[binding.length - 1];
-    }
+  static get properties() {
+    return {
+    /** @type {Array<string>} */
+      binding: Array,
+    };
   }
 
-  customElements.define(GrKeyBindingDisplay.is, GrKeyBindingDisplay);
-})();
+  _computeModifiers(binding) {
+    return binding.slice(0, binding.length - 1);
+  }
+
+  _computeKey(binding) {
+    return binding[binding.length - 1];
+  }
+}
+
+customElements.define(GrKeyBindingDisplay.is, GrKeyBindingDisplay);
diff --git a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_html.js b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_html.js
index a863276..f98be3a 100644
--- a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_html.js
+++ b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_html.js
@@ -1,25 +1,22 @@
-<!--
-@license
-Copyright (C) 2018 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.
+ */
+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.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-key-binding-display">
-  <template>
+export const htmlTemplate = html`
     <style include="shared-styles">
       .key {
         background-color: var(--chip-background-color);
@@ -35,14 +32,9 @@
       <template is="dom-if" if="[[index]]">
         or
       </template>
-      <template
-          is="dom-repeat"
-          items="[[_computeModifiers(item)]]"
-          as="modifier">
+      <template is="dom-repeat" items="[[_computeModifiers(item)]]" as="modifier">
         <span class="key modifier">[[modifier]]</span>
       </template>
       <span class="key">[[_computeKey(item)]]</span>
     </template>
-  </template>
-  <script src="gr-key-binding-display.js"></script>
-</dom-module>
+`;
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
index f682f0a..bb449c3 100644
--- 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
@@ -18,15 +18,20 @@
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-key-binding-display</title>
 
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-key-binding-display.html">
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
+<script type="module" src="../../../test/test-pre-setup.js"></script>
+<script type="module" src="../../../test/common-test-setup.js"></script>
+<script type="module" src="./gr-key-binding-display.js"></script>
 
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-key-binding-display.js';
+void(0);
+</script>
 
 <test-fixture id="basic">
   <template>
@@ -34,37 +39,39 @@
   </template>
 </test-fixture>
 
-<script>
-  suite('gr-key-binding-display tests', async () => {
-    await readyToTest();
-    let element;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-key-binding-display.js';
+suite('gr-key-binding-display tests', () => {
+  let element;
 
-    setup(() => {
-      element = fixture('basic');
+  setup(() => {
+    element = fixture('basic');
+  });
+
+  suite('_computeKey', () => {
+    test('unmodified key', () => {
+      assert.strictEqual(element._computeKey(['x']), 'x');
     });
 
-    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']);
-      });
+    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-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js
index 4630ca7..371ba02 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
@@ -14,129 +14,140 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-(function() {
-  'use strict';
+import '../../../scripts/bundled-polymer.js';
 
-  const {ShortcutSection} = window.Gerrit.KeyboardShortcutBinder;
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import '../../shared/gr-button/gr-button.js';
+import '../gr-key-binding-display/gr-key-binding-display.js';
+import '../../../styles/shared-styles.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-keyboard-shortcuts-dialog_html.js';
 
+const {ShortcutSection} = window.Gerrit.KeyboardShortcutBinder;
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.KeyboardShortcutMixin
+ * @extends Polymer.Element
+ */
+class GrKeyboardShortcutsDialog extends mixinBehaviors( [
+  Gerrit.FireBehavior,
+  Gerrit.KeyboardShortcutBehavior,
+], GestureEventListeners(
+    LegacyElementMixin(
+        PolymerElement))) {
+  static get template() { return htmlTemplate; }
+
+  static get is() { return 'gr-keyboard-shortcuts-dialog'; }
   /**
-   * @appliesMixin Gerrit.FireMixin
-   * @appliesMixin Gerrit.KeyboardShortcutMixin
-   * @extends Polymer.Element
+   * Fired when the user presses the close button.
+   *
+   * @event close
    */
-  class GrKeyboardShortcutsDialog extends Polymer.mixinBehaviors( [
-    Gerrit.FireBehavior,
-    Gerrit.KeyboardShortcutBehavior,
-  ], Polymer.GestureEventListeners(
-      Polymer.LegacyElementMixin(
-          Polymer.Element))) {
-    static get is() { return 'gr-keyboard-shortcuts-dialog'; }
-    /**
-     * Fired when the user presses the close button.
-     *
-     * @event close
-     */
 
-    static get properties() {
-      return {
-        _left: Array,
-        _right: Array,
+  static get properties() {
+    return {
+      _left: Array,
+      _right: Array,
 
-        _propertyBySection: {
-          type: Object,
-          value() {
-            return {
-              [ShortcutSection.EVERYWHERE]: '_everywhere',
-              [ShortcutSection.NAVIGATION]: '_navigation',
-              [ShortcutSection.DASHBOARD]: '_dashboard',
-              [ShortcutSection.CHANGE_LIST]: '_changeList',
-              [ShortcutSection.ACTIONS]: '_actions',
-              [ShortcutSection.REPLY_DIALOG]: '_replyDialog',
-              [ShortcutSection.FILE_LIST]: '_fileList',
-              [ShortcutSection.DIFFS]: '_diffs',
-            };
-          },
+      _propertyBySection: {
+        type: Object,
+        value() {
+          return {
+            [ShortcutSection.EVERYWHERE]: '_everywhere',
+            [ShortcutSection.NAVIGATION]: '_navigation',
+            [ShortcutSection.DASHBOARD]: '_dashboard',
+            [ShortcutSection.CHANGE_LIST]: '_changeList',
+            [ShortcutSection.ACTIONS]: '_actions',
+            [ShortcutSection.REPLY_DIALOG]: '_replyDialog',
+            [ShortcutSection.FILE_LIST]: '_fileList',
+            [ShortcutSection.DIFFS]: '_diffs',
+          };
         },
-      };
-    }
-
-    /** @override */
-    ready() {
-      super.ready();
-      this._ensureAttribute('role', 'dialog');
-    }
-
-    /** @override */
-    attached() {
-      super.attached();
-      this.addKeyboardShortcutDirectoryListener(
-          this._onDirectoryUpdated.bind(this));
-    }
-
-    /** @override */
-    detached() {
-      super.detached();
-      this.removeKeyboardShortcutDirectoryListener(
-          this._onDirectoryUpdated.bind(this));
-    }
-
-    _handleCloseTap(e) {
-      e.preventDefault();
-      e.stopPropagation();
-      this.fire('close', null, {bubbles: false});
-    }
-
-    _onDirectoryUpdated(directory) {
-      const left = [];
-      const right = [];
-
-      if (directory.has(ShortcutSection.EVERYWHERE)) {
-        left.push({
-          section: ShortcutSection.EVERYWHERE,
-          shortcuts: directory.get(ShortcutSection.EVERYWHERE),
-        });
-      }
-
-      if (directory.has(ShortcutSection.NAVIGATION)) {
-        left.push({
-          section: ShortcutSection.NAVIGATION,
-          shortcuts: directory.get(ShortcutSection.NAVIGATION),
-        });
-      }
-
-      if (directory.has(ShortcutSection.ACTIONS)) {
-        right.push({
-          section: ShortcutSection.ACTIONS,
-          shortcuts: directory.get(ShortcutSection.ACTIONS),
-        });
-      }
-
-      if (directory.has(ShortcutSection.REPLY_DIALOG)) {
-        right.push({
-          section: ShortcutSection.REPLY_DIALOG,
-          shortcuts: directory.get(ShortcutSection.REPLY_DIALOG),
-        });
-      }
-
-      if (directory.has(ShortcutSection.FILE_LIST)) {
-        right.push({
-          section: ShortcutSection.FILE_LIST,
-          shortcuts: directory.get(ShortcutSection.FILE_LIST),
-        });
-      }
-
-      if (directory.has(ShortcutSection.DIFFS)) {
-        right.push({
-          section: ShortcutSection.DIFFS,
-          shortcuts: directory.get(ShortcutSection.DIFFS),
-        });
-      }
-
-      this.set('_left', left);
-      this.set('_right', right);
-    }
+      },
+    };
   }
 
-  customElements.define(GrKeyboardShortcutsDialog.is,
-      GrKeyboardShortcutsDialog);
-})();
+  /** @override */
+  ready() {
+    super.ready();
+    this._ensureAttribute('role', 'dialog');
+  }
+
+  /** @override */
+  attached() {
+    super.attached();
+    this.addKeyboardShortcutDirectoryListener(
+        this._onDirectoryUpdated.bind(this));
+  }
+
+  /** @override */
+  detached() {
+    super.detached();
+    this.removeKeyboardShortcutDirectoryListener(
+        this._onDirectoryUpdated.bind(this));
+  }
+
+  _handleCloseTap(e) {
+    e.preventDefault();
+    e.stopPropagation();
+    this.fire('close', null, {bubbles: false});
+  }
+
+  _onDirectoryUpdated(directory) {
+    const left = [];
+    const right = [];
+
+    if (directory.has(ShortcutSection.EVERYWHERE)) {
+      left.push({
+        section: ShortcutSection.EVERYWHERE,
+        shortcuts: directory.get(ShortcutSection.EVERYWHERE),
+      });
+    }
+
+    if (directory.has(ShortcutSection.NAVIGATION)) {
+      left.push({
+        section: ShortcutSection.NAVIGATION,
+        shortcuts: directory.get(ShortcutSection.NAVIGATION),
+      });
+    }
+
+    if (directory.has(ShortcutSection.ACTIONS)) {
+      right.push({
+        section: ShortcutSection.ACTIONS,
+        shortcuts: directory.get(ShortcutSection.ACTIONS),
+      });
+    }
+
+    if (directory.has(ShortcutSection.REPLY_DIALOG)) {
+      right.push({
+        section: ShortcutSection.REPLY_DIALOG,
+        shortcuts: directory.get(ShortcutSection.REPLY_DIALOG),
+      });
+    }
+
+    if (directory.has(ShortcutSection.FILE_LIST)) {
+      right.push({
+        section: ShortcutSection.FILE_LIST,
+        shortcuts: directory.get(ShortcutSection.FILE_LIST),
+      });
+    }
+
+    if (directory.has(ShortcutSection.DIFFS)) {
+      right.push({
+        section: ShortcutSection.DIFFS,
+        shortcuts: directory.get(ShortcutSection.DIFFS),
+      });
+    }
+
+    this.set('_left', left);
+    this.set('_right', right);
+  }
+}
+
+customElements.define(GrKeyboardShortcutsDialog.is,
+    GrKeyboardShortcutsDialog);
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_html.js b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_html.js
index a4424a2..380228f 100644
--- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_html.js
+++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_html.js
@@ -1,29 +1,22 @@
-<!--
-@license
-Copyright (C) 2016 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.
+ */
+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.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../gr-key-binding-display/gr-key-binding-display.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-keyboard-shortcuts-dialog">
-  <template>
+export const htmlTemplate = html`
     <style include="shared-styles">
       :host {
         display: block;
@@ -63,7 +56,7 @@
     </style>
     <header>
       <h3>Keyboard shortcuts</h3>
-      <gr-button link on-click="_handleCloseTap">Close</gr-button>
+      <gr-button link="" on-click="_handleCloseTap">Close</gr-button>
     </header>
     <main>
       <table>
@@ -106,6 +99,4 @@
       </template>
     </main>
     <footer></footer>
-  </template>
-  <script src="gr-keyboard-shortcuts-dialog.js"></script>
-</dom-module>
+`;
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.html
index cc53db17..eedd166 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.html
@@ -18,15 +18,20 @@
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-key-binding-display</title>
 
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-keyboard-shortcuts-dialog.html">
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
+<script type="module" src="../../../test/test-pre-setup.js"></script>
+<script type="module" src="../../../test/common-test-setup.js"></script>
+<script type="module" src="./gr-keyboard-shortcuts-dialog.js"></script>
 
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-keyboard-shortcuts-dialog.js';
+void(0);
+</script>
 
 <test-fixture id="basic">
   <template>
@@ -34,150 +39,152 @@
   </template>
 </test-fixture>
 
-<script>
-  suite('gr-keyboard-shortcuts-dialog tests', async () => {
-    await readyToTest();
-    const kb = window.Gerrit.KeyboardShortcutBinder;
-    let element;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-keyboard-shortcuts-dialog.js';
+suite('gr-keyboard-shortcuts-dialog tests', () => {
+  const kb = window.Gerrit.KeyboardShortcutBinder;
+  let element;
 
-    setup(() => {
-      element = fixture('basic');
+  setup(() => {
+    element = fixture('basic');
+  });
+
+  function update(directory) {
+    element._onDirectoryUpdated(directory);
+    flushAsynchronousOperations();
+  }
+
+  suite('_left and _right contents', () => {
+    test('empty dialog', () => {
+      assert.strictEqual(element._left.length, 0);
+      assert.strictEqual(element._right.length, 0);
     });
 
-    function update(directory) {
-      element._onDirectoryUpdated(directory);
-      flushAsynchronousOperations();
-    }
+    test('everywhere goes on left', () => {
+      update(new Map([
+        [kb.ShortcutSection.EVERYWHERE, ['everywhere shortcuts']],
+      ]));
+      assert.deepEqual(
+          element._left,
+          [
+            {
+              section: kb.ShortcutSection.EVERYWHERE,
+              shortcuts: ['everywhere shortcuts'],
+            },
+          ]);
+      assert.strictEqual(element._right.length, 0);
+    });
 
-    suite('_left and _right contents', () => {
-      test('empty dialog', () => {
-        assert.strictEqual(element._left.length, 0);
-        assert.strictEqual(element._right.length, 0);
-      });
+    test('navigation goes on left', () => {
+      update(new Map([
+        [kb.ShortcutSection.NAVIGATION, ['navigation shortcuts']],
+      ]));
+      assert.deepEqual(
+          element._left,
+          [
+            {
+              section: kb.ShortcutSection.NAVIGATION,
+              shortcuts: ['navigation shortcuts'],
+            },
+          ]);
+      assert.strictEqual(element._right.length, 0);
+    });
 
-      test('everywhere goes on left', () => {
-        update(new Map([
-          [kb.ShortcutSection.EVERYWHERE, ['everywhere shortcuts']],
-        ]));
-        assert.deepEqual(
-            element._left,
-            [
-              {
-                section: kb.ShortcutSection.EVERYWHERE,
-                shortcuts: ['everywhere shortcuts'],
-              },
-            ]);
-        assert.strictEqual(element._right.length, 0);
-      });
+    test('actions go on right', () => {
+      update(new Map([
+        [kb.ShortcutSection.ACTIONS, ['actions shortcuts']],
+      ]));
+      assert.deepEqual(
+          element._right,
+          [
+            {
+              section: kb.ShortcutSection.ACTIONS,
+              shortcuts: ['actions shortcuts'],
+            },
+          ]);
+      assert.strictEqual(element._left.length, 0);
+    });
 
-      test('navigation goes on left', () => {
-        update(new Map([
-          [kb.ShortcutSection.NAVIGATION, ['navigation shortcuts']],
-        ]));
-        assert.deepEqual(
-            element._left,
-            [
-              {
-                section: kb.ShortcutSection.NAVIGATION,
-                shortcuts: ['navigation shortcuts'],
-              },
-            ]);
-        assert.strictEqual(element._right.length, 0);
-      });
+    test('reply dialog goes on right', () => {
+      update(new Map([
+        [kb.ShortcutSection.REPLY_DIALOG, ['reply dialog shortcuts']],
+      ]));
+      assert.deepEqual(
+          element._right,
+          [
+            {
+              section: kb.ShortcutSection.REPLY_DIALOG,
+              shortcuts: ['reply dialog shortcuts'],
+            },
+          ]);
+      assert.strictEqual(element._left.length, 0);
+    });
 
-      test('actions go on right', () => {
-        update(new Map([
-          [kb.ShortcutSection.ACTIONS, ['actions shortcuts']],
-        ]));
-        assert.deepEqual(
-            element._right,
-            [
-              {
-                section: kb.ShortcutSection.ACTIONS,
-                shortcuts: ['actions shortcuts'],
-              },
-            ]);
-        assert.strictEqual(element._left.length, 0);
-      });
+    test('file list goes on right', () => {
+      update(new Map([
+        [kb.ShortcutSection.FILE_LIST, ['file list shortcuts']],
+      ]));
+      assert.deepEqual(
+          element._right,
+          [
+            {
+              section: kb.ShortcutSection.FILE_LIST,
+              shortcuts: ['file list shortcuts'],
+            },
+          ]);
+      assert.strictEqual(element._left.length, 0);
+    });
 
-      test('reply dialog goes on right', () => {
-        update(new Map([
-          [kb.ShortcutSection.REPLY_DIALOG, ['reply dialog shortcuts']],
-        ]));
-        assert.deepEqual(
-            element._right,
-            [
-              {
-                section: kb.ShortcutSection.REPLY_DIALOG,
-                shortcuts: ['reply dialog shortcuts'],
-              },
-            ]);
-        assert.strictEqual(element._left.length, 0);
-      });
+    test('diffs go on right', () => {
+      update(new Map([
+        [kb.ShortcutSection.DIFFS, ['diffs shortcuts']],
+      ]));
+      assert.deepEqual(
+          element._right,
+          [
+            {
+              section: kb.ShortcutSection.DIFFS,
+              shortcuts: ['diffs shortcuts'],
+            },
+          ]);
+      assert.strictEqual(element._left.length, 0);
+    });
 
-      test('file list goes on right', () => {
-        update(new Map([
-          [kb.ShortcutSection.FILE_LIST, ['file list shortcuts']],
-        ]));
-        assert.deepEqual(
-            element._right,
-            [
-              {
-                section: kb.ShortcutSection.FILE_LIST,
-                shortcuts: ['file list shortcuts'],
-              },
-            ]);
-        assert.strictEqual(element._left.length, 0);
-      });
-
-      test('diffs go on right', () => {
-        update(new Map([
-          [kb.ShortcutSection.DIFFS, ['diffs shortcuts']],
-        ]));
-        assert.deepEqual(
-            element._right,
-            [
-              {
-                section: kb.ShortcutSection.DIFFS,
-                shortcuts: ['diffs shortcuts'],
-              },
-            ]);
-        assert.strictEqual(element._left.length, 0);
-      });
-
-      test('multiple sections on each side', () => {
-        update(new Map([
-          [kb.ShortcutSection.ACTIONS, ['actions shortcuts']],
-          [kb.ShortcutSection.DIFFS, ['diffs shortcuts']],
-          [kb.ShortcutSection.EVERYWHERE, ['everywhere shortcuts']],
-          [kb.ShortcutSection.NAVIGATION, ['navigation shortcuts']],
-        ]));
-        assert.deepEqual(
-            element._left,
-            [
-              {
-                section: kb.ShortcutSection.EVERYWHERE,
-                shortcuts: ['everywhere shortcuts'],
-              },
-              {
-                section: kb.ShortcutSection.NAVIGATION,
-                shortcuts: ['navigation shortcuts'],
-              },
-            ]);
-        assert.deepEqual(
-            element._right,
-            [
-              {
-                section: kb.ShortcutSection.ACTIONS,
-                shortcuts: ['actions shortcuts'],
-              },
-              {
-                section: kb.ShortcutSection.DIFFS,
-                shortcuts: ['diffs shortcuts'],
-              },
-            ]);
-      });
+    test('multiple sections on each side', () => {
+      update(new Map([
+        [kb.ShortcutSection.ACTIONS, ['actions shortcuts']],
+        [kb.ShortcutSection.DIFFS, ['diffs shortcuts']],
+        [kb.ShortcutSection.EVERYWHERE, ['everywhere shortcuts']],
+        [kb.ShortcutSection.NAVIGATION, ['navigation shortcuts']],
+      ]));
+      assert.deepEqual(
+          element._left,
+          [
+            {
+              section: kb.ShortcutSection.EVERYWHERE,
+              shortcuts: ['everywhere shortcuts'],
+            },
+            {
+              section: kb.ShortcutSection.NAVIGATION,
+              shortcuts: ['navigation shortcuts'],
+            },
+          ]);
+      assert.deepEqual(
+          element._right,
+          [
+            {
+              section: kb.ShortcutSection.ACTIONS,
+              shortcuts: ['actions shortcuts'],
+            },
+            {
+              section: kb.ShortcutSection.DIFFS,
+              shortcuts: ['diffs shortcuts'],
+            },
+          ]);
     });
   });
+});
 </script>
 
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
index 05765fb..c8ed50c 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
@@ -14,332 +14,349 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-(function() {
-  'use strict';
+import '../../../scripts/bundled-polymer.js';
 
-  const DEFAULT_LINKS = [{
-    title: 'Changes',
-    links: [
-      {
-        url: '/q/status:open',
-        name: 'Open',
+import '../../../behaviors/docs-url-behavior/docs-url-behavior.js';
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.js';
+import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
+import '../../shared/gr-dropdown/gr-dropdown.js';
+import '../../shared/gr-icons/gr-icons.js';
+import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-account-dropdown/gr-account-dropdown.js';
+import '../gr-smart-search/gr-smart-search.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-main-header_html.js';
+
+const DEFAULT_LINKS = [{
+  title: 'Changes',
+  links: [
+    {
+      url: '/q/status:open',
+      name: 'Open',
+    },
+    {
+      url: '/q/status:merged',
+      name: 'Merged',
+    },
+    {
+      url: '/q/status:abandoned',
+      name: 'Abandoned',
+    },
+  ],
+}];
+
+const DOCUMENTATION_LINKS = [
+  {
+    url: '/index.html',
+    name: 'Table of Contents',
+  },
+  {
+    url: '/user-search.html',
+    name: 'Searching',
+  },
+  {
+    url: '/user-upload.html',
+    name: 'Uploading',
+  },
+  {
+    url: '/access-control.html',
+    name: 'Access Control',
+  },
+  {
+    url: '/rest-api.html',
+    name: 'REST API',
+  },
+  {
+    url: '/intro-project-owner.html',
+    name: 'Project Owner Guide',
+  },
+];
+
+// Set of authentication methods that can provide custom registration page.
+const AUTH_TYPES_WITH_REGISTER_URL = new Set([
+  'LDAP',
+  'LDAP_BIND',
+  'CUSTOM_EXTENSION',
+]);
+
+/**
+ * @appliesMixin Gerrit.AdminNavMixin
+ * @appliesMixin Gerrit.BaseUrlMixin
+ * @appliesMixin Gerrit.DocsUrlMixin
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrMainHeader extends mixinBehaviors( [
+  Gerrit.AdminNavBehavior,
+  Gerrit.BaseUrlBehavior,
+  Gerrit.DocsUrlBehavior,
+  Gerrit.FireBehavior,
+], GestureEventListeners(
+    LegacyElementMixin(
+        PolymerElement))) {
+  static get template() { return htmlTemplate; }
+
+  static get is() { return 'gr-main-header'; }
+
+  static get properties() {
+    return {
+      searchQuery: {
+        type: String,
+        notify: true,
       },
-      {
-        url: '/q/status:merged',
-        name: 'Merged',
+      loggedIn: {
+        type: Boolean,
+        reflectToAttribute: true,
       },
-      {
-        url: '/q/status:abandoned',
-        name: 'Abandoned',
+      loading: {
+        type: Boolean,
+        reflectToAttribute: true,
       },
-    ],
-  }];
 
-  const DOCUMENTATION_LINKS = [
-    {
-      url: '/index.html',
-      name: 'Table of Contents',
-    },
-    {
-      url: '/user-search.html',
-      name: 'Searching',
-    },
-    {
-      url: '/user-upload.html',
-      name: 'Uploading',
-    },
-    {
-      url: '/access-control.html',
-      name: 'Access Control',
-    },
-    {
-      url: '/rest-api.html',
-      name: 'REST API',
-    },
-    {
-      url: '/intro-project-owner.html',
-      name: 'Project Owner Guide',
-    },
-  ];
+      /** @type {?Object} */
+      _account: Object,
+      _adminLinks: {
+        type: Array,
+        value() { return []; },
+      },
+      _defaultLinks: {
+        type: Array,
+        value() {
+          return DEFAULT_LINKS;
+        },
+      },
+      _docBaseUrl: {
+        type: String,
+        value: null,
+      },
+      _links: {
+        type: Array,
+        computed: '_computeLinks(_defaultLinks, _userLinks, _adminLinks, ' +
+          '_topMenus, _docBaseUrl)',
+      },
+      loginUrl: {
+        type: String,
+        value: '/login',
+      },
+      _userLinks: {
+        type: Array,
+        value() { return []; },
+      },
+      _topMenus: {
+        type: Array,
+        value() { return []; },
+      },
+      _registerText: {
+        type: String,
+        value: 'Sign up',
+      },
+      _registerURL: {
+        type: String,
+        value: null,
+      },
+    };
+  }
 
-  // Set of authentication methods that can provide custom registration page.
-  const AUTH_TYPES_WITH_REGISTER_URL = new Set([
-    'LDAP',
-    'LDAP_BIND',
-    'CUSTOM_EXTENSION',
-  ]);
+  static get observers() {
+    return [
+      '_accountLoaded(_account)',
+    ];
+  }
 
-  /**
-   * @appliesMixin Gerrit.AdminNavMixin
-   * @appliesMixin Gerrit.BaseUrlMixin
-   * @appliesMixin Gerrit.DocsUrlMixin
-   * @appliesMixin Gerrit.FireMixin
-   * @extends Polymer.Element
-   */
-  class GrMainHeader extends Polymer.mixinBehaviors( [
-    Gerrit.AdminNavBehavior,
-    Gerrit.BaseUrlBehavior,
-    Gerrit.DocsUrlBehavior,
-    Gerrit.FireBehavior,
-  ], Polymer.GestureEventListeners(
-      Polymer.LegacyElementMixin(
-          Polymer.Element))) {
-    static get is() { return 'gr-main-header'; }
+  /** @override */
+  ready() {
+    super.ready();
+    this._ensureAttribute('role', 'banner');
+  }
 
-    static get properties() {
+  /** @override */
+  attached() {
+    super.attached();
+    this._loadAccount();
+    this._loadConfig();
+  }
+
+  /** @override */
+  detached() {
+    super.detached();
+  }
+
+  reload() {
+    this._loadAccount();
+  }
+
+  _computeRelativeURL(path) {
+    return '//' + window.location.host + this.getBaseUrl() + path;
+  }
+
+  _computeLinks(defaultLinks, userLinks, adminLinks, topMenus, docBaseUrl) {
+    // Polymer 2: check for undefined
+    if ([
+      defaultLinks,
+      userLinks,
+      adminLinks,
+      topMenus,
+      docBaseUrl,
+    ].some(arg => arg === undefined)) {
+      return undefined;
+    }
+
+    const links = defaultLinks.map(menu => {
       return {
-        searchQuery: {
-          type: String,
-          notify: true,
-        },
-        loggedIn: {
-          type: Boolean,
-          reflectToAttribute: true,
-        },
-        loading: {
-          type: Boolean,
-          reflectToAttribute: true,
-        },
-
-        /** @type {?Object} */
-        _account: Object,
-        _adminLinks: {
-          type: Array,
-          value() { return []; },
-        },
-        _defaultLinks: {
-          type: Array,
-          value() {
-            return DEFAULT_LINKS;
-          },
-        },
-        _docBaseUrl: {
-          type: String,
-          value: null,
-        },
-        _links: {
-          type: Array,
-          computed: '_computeLinks(_defaultLinks, _userLinks, _adminLinks, ' +
-            '_topMenus, _docBaseUrl)',
-        },
-        loginUrl: {
-          type: String,
-          value: '/login',
-        },
-        _userLinks: {
-          type: Array,
-          value() { return []; },
-        },
-        _topMenus: {
-          type: Array,
-          value() { return []; },
-        },
-        _registerText: {
-          type: String,
-          value: 'Sign up',
-        },
-        _registerURL: {
-          type: String,
-          value: null,
-        },
+        title: menu.title,
+        links: menu.links.slice(),
       };
-    }
-
-    static get observers() {
-      return [
-        '_accountLoaded(_account)',
-      ];
-    }
-
-    /** @override */
-    ready() {
-      super.ready();
-      this._ensureAttribute('role', 'banner');
-    }
-
-    /** @override */
-    attached() {
-      super.attached();
-      this._loadAccount();
-      this._loadConfig();
-    }
-
-    /** @override */
-    detached() {
-      super.detached();
-    }
-
-    reload() {
-      this._loadAccount();
-    }
-
-    _computeRelativeURL(path) {
-      return '//' + window.location.host + this.getBaseUrl() + path;
-    }
-
-    _computeLinks(defaultLinks, userLinks, adminLinks, topMenus, docBaseUrl) {
-      // Polymer 2: check for undefined
-      if ([
-        defaultLinks,
-        userLinks,
-        adminLinks,
-        topMenus,
-        docBaseUrl,
-      ].some(arg => arg === undefined)) {
-        return undefined;
-      }
-
-      const links = defaultLinks.map(menu => {
-        return {
-          title: menu.title,
-          links: menu.links.slice(),
-        };
-      });
-      if (userLinks && userLinks.length > 0) {
-        links.push({
-          title: 'Your',
-          links: userLinks.slice(),
-        });
-      }
-      const docLinks = this._getDocLinks(docBaseUrl, DOCUMENTATION_LINKS);
-      if (docLinks.length) {
-        links.push({
-          title: 'Documentation',
-          links: docLinks,
-          class: 'hideOnMobile',
-        });
-      }
+    });
+    if (userLinks && userLinks.length > 0) {
       links.push({
-        title: 'Browse',
-        links: adminLinks.slice(),
+        title: 'Your',
+        links: userLinks.slice(),
       });
-      const topMenuLinks = [];
-      links.forEach(link => { topMenuLinks[link.title] = link.links; });
-      for (const m of topMenus) {
-        const items = m.items.map(this._fixCustomMenuItem).filter(link =>
-          // Ignore GWT project links
-          !link.url.includes('${projectName}')
-        );
-        if (m.name in topMenuLinks) {
-          items.forEach(link => { topMenuLinks[m.name].push(link); });
-        } else {
-          links.push({
-            title: m.name,
-            links: topMenuLinks[m.name] = items,
+    }
+    const docLinks = this._getDocLinks(docBaseUrl, DOCUMENTATION_LINKS);
+    if (docLinks.length) {
+      links.push({
+        title: 'Documentation',
+        links: docLinks,
+        class: 'hideOnMobile',
+      });
+    }
+    links.push({
+      title: 'Browse',
+      links: adminLinks.slice(),
+    });
+    const topMenuLinks = [];
+    links.forEach(link => { topMenuLinks[link.title] = link.links; });
+    for (const m of topMenus) {
+      const items = m.items.map(this._fixCustomMenuItem).filter(link =>
+        // Ignore GWT project links
+        !link.url.includes('${projectName}')
+      );
+      if (m.name in topMenuLinks) {
+        items.forEach(link => { topMenuLinks[m.name].push(link); });
+      } else {
+        links.push({
+          title: m.name,
+          links: topMenuLinks[m.name] = items,
+        });
+      }
+    }
+    return links;
+  }
+
+  _getDocLinks(docBaseUrl, docLinks) {
+    if (!docBaseUrl || !docLinks) {
+      return [];
+    }
+    return docLinks.map(link => {
+      let url = docBaseUrl;
+      if (url && url[url.length - 1] === '/') {
+        url = url.substring(0, url.length - 1);
+      }
+      return {
+        url: url + link.url,
+        name: link.name,
+        target: '_blank',
+      };
+    });
+  }
+
+  _loadAccount() {
+    this.loading = true;
+    const promises = [
+      this.$.restAPI.getAccount(),
+      this.$.restAPI.getTopMenus(),
+      Gerrit.awaitPluginsLoaded(),
+    ];
+
+    return Promise.all(promises).then(result => {
+      const account = result[0];
+      this._account = account;
+      this.loggedIn = !!account;
+      this.loading = false;
+      this._topMenus = result[1];
+
+      return this.getAdminLinks(account,
+          this.$.restAPI.getAccountCapabilities.bind(this.$.restAPI),
+          this.$.jsAPI.getAdminMenuLinks.bind(this.$.jsAPI))
+          .then(res => {
+            this._adminLinks = res.links;
           });
-        }
+    });
+  }
+
+  _loadConfig() {
+    this.$.restAPI.getConfig()
+        .then(config => {
+          this._retrieveRegisterURL(config);
+          return this.getDocsBaseUrl(config, this.$.restAPI);
+        })
+        .then(docBaseUrl => { this._docBaseUrl = docBaseUrl; });
+  }
+
+  _accountLoaded(account) {
+    if (!account) { return; }
+
+    this.$.restAPI.getPreferences().then(prefs => {
+      this._userLinks = prefs && prefs.my ?
+        prefs.my.map(this._fixCustomMenuItem) : [];
+    });
+  }
+
+  _retrieveRegisterURL(config) {
+    if (AUTH_TYPES_WITH_REGISTER_URL.has(config.auth.auth_type)) {
+      this._registerURL = config.auth.register_url;
+      if (config.auth.register_text) {
+        this._registerText = config.auth.register_text;
       }
-      return links;
-    }
-
-    _getDocLinks(docBaseUrl, docLinks) {
-      if (!docBaseUrl || !docLinks) {
-        return [];
-      }
-      return docLinks.map(link => {
-        let url = docBaseUrl;
-        if (url && url[url.length - 1] === '/') {
-          url = url.substring(0, url.length - 1);
-        }
-        return {
-          url: url + link.url,
-          name: link.name,
-          target: '_blank',
-        };
-      });
-    }
-
-    _loadAccount() {
-      this.loading = true;
-      const promises = [
-        this.$.restAPI.getAccount(),
-        this.$.restAPI.getTopMenus(),
-        Gerrit.awaitPluginsLoaded(),
-      ];
-
-      return Promise.all(promises).then(result => {
-        const account = result[0];
-        this._account = account;
-        this.loggedIn = !!account;
-        this.loading = false;
-        this._topMenus = result[1];
-
-        return this.getAdminLinks(account,
-            this.$.restAPI.getAccountCapabilities.bind(this.$.restAPI),
-            this.$.jsAPI.getAdminMenuLinks.bind(this.$.jsAPI))
-            .then(res => {
-              this._adminLinks = res.links;
-            });
-      });
-    }
-
-    _loadConfig() {
-      this.$.restAPI.getConfig()
-          .then(config => {
-            this._retrieveRegisterURL(config);
-            return this.getDocsBaseUrl(config, this.$.restAPI);
-          })
-          .then(docBaseUrl => { this._docBaseUrl = docBaseUrl; });
-    }
-
-    _accountLoaded(account) {
-      if (!account) { return; }
-
-      this.$.restAPI.getPreferences().then(prefs => {
-        this._userLinks = prefs && prefs.my ?
-          prefs.my.map(this._fixCustomMenuItem) : [];
-      });
-    }
-
-    _retrieveRegisterURL(config) {
-      if (AUTH_TYPES_WITH_REGISTER_URL.has(config.auth.auth_type)) {
-        this._registerURL = config.auth.register_url;
-        if (config.auth.register_text) {
-          this._registerText = config.auth.register_text;
-        }
-      }
-    }
-
-    _computeIsInvisible(registerURL) {
-      return registerURL ? '' : 'invisible';
-    }
-
-    _fixCustomMenuItem(linkObj) {
-      // Normalize all urls to PolyGerrit style.
-      if (linkObj.url.startsWith('#')) {
-        linkObj.url = linkObj.url.slice(1);
-      }
-
-      // Delete target property due to complications of
-      // https://bugs.chromium.org/p/gerrit/issues/detail?id=5888
-      //
-      // The server tries to guess whether URL is a view within the UI.
-      // If not, it sets target='_blank' on the menu item. The server
-      // makes assumptions that work for the GWT UI, but not PolyGerrit,
-      // so we'll just disable it altogether for now.
-      delete linkObj.target;
-
-      return linkObj;
-    }
-
-    _generateSettingsLink() {
-      return this.getBaseUrl() + '/settings/';
-    }
-
-    _onMobileSearchTap(e) {
-      e.preventDefault();
-      e.stopPropagation();
-      this.fire('mobile-search', null, {bubbles: false});
-    }
-
-    _computeLinkGroupClass(linkGroup) {
-      if (linkGroup && linkGroup.class) {
-        return linkGroup.class;
-      }
-
-      return '';
     }
   }
 
-  customElements.define(GrMainHeader.is, GrMainHeader);
-})();
+  _computeIsInvisible(registerURL) {
+    return registerURL ? '' : 'invisible';
+  }
+
+  _fixCustomMenuItem(linkObj) {
+    // Normalize all urls to PolyGerrit style.
+    if (linkObj.url.startsWith('#')) {
+      linkObj.url = linkObj.url.slice(1);
+    }
+
+    // Delete target property due to complications of
+    // https://bugs.chromium.org/p/gerrit/issues/detail?id=5888
+    //
+    // The server tries to guess whether URL is a view within the UI.
+    // If not, it sets target='_blank' on the menu item. The server
+    // makes assumptions that work for the GWT UI, but not PolyGerrit,
+    // so we'll just disable it altogether for now.
+    delete linkObj.target;
+
+    return linkObj;
+  }
+
+  _generateSettingsLink() {
+    return this.getBaseUrl() + '/settings/';
+  }
+
+  _onMobileSearchTap(e) {
+    e.preventDefault();
+    e.stopPropagation();
+    this.fire('mobile-search', null, {bubbles: false});
+  }
+
+  _computeLinkGroupClass(linkGroup) {
+    if (linkGroup && linkGroup.class) {
+      return linkGroup.class;
+    }
+
+    return '';
+  }
+}
+
+customElements.define(GrMainHeader.is, GrMainHeader);
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_html.js b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_html.js
index 51717a8..307a081 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_html.js
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_html.js
@@ -1,35 +1,22 @@
-<!--
-@license
-Copyright (C) 2016 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.
+ */
+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.
--->
-<link rel="import" href="/bower_components/polymer/polymer.html">
-
-<link rel="import" href="../../../behaviors/docs-url-behavior/docs-url-behavior.html">
-<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.html">
-<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
-<link rel="import" href="../../shared/gr-dropdown/gr-dropdown.html">
-<link rel="import" href="../../shared/gr-icons/gr-icons.html">
-<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../gr-account-dropdown/gr-account-dropdown.html">
-<link rel="import" href="../gr-smart-search/gr-smart-search.html">
-
-<dom-module id="gr-main-header">
-  <template>
+export const htmlTemplate = html`
     <style include="shared-styles">
       :host {
         display: block;
@@ -183,19 +170,15 @@
       }
     </style>
     <nav>
-      <a href$="[[_computeRelativeURL('/')]]" class="bigTitle">
+      <a href\$="[[_computeRelativeURL('/')]]" class="bigTitle">
         <gr-endpoint-decorator name="header-title">
           <span class="titleText"></span>
         </gr-endpoint-decorator>
       </a>
       <ul class="links">
         <template is="dom-repeat" items="[[_links]]" as="linkGroup">
-          <li class$="[[_computeLinkGroupClass(linkGroup)]]">
-            <gr-dropdown
-                link
-                down-arrow
-                items = [[linkGroup.links]]
-                horizontal-align="left">
+          <li class\$="[[_computeLinkGroupClass(linkGroup)]]">
+            <gr-dropdown link="" down-arrow="" items="[[linkGroup.links]]" horizontal-align="left">
               <span class="linksTitle" id="[[linkGroup.title]]">
                 [[linkGroup.title]]
               </span>
@@ -204,29 +187,18 @@
         </template>
       </ul>
       <div class="rightItems">
-        <gr-endpoint-decorator
-            class="hideOnMobile"
-            name="header-small-banner"></gr-endpoint-decorator>
-        <gr-smart-search
-            id="search"
-            search-query="{{searchQuery}}"></gr-smart-search>
-        <gr-endpoint-decorator
-            class="hideOnMobile"
-            name="header-browse-source"></gr-endpoint-decorator>
+        <gr-endpoint-decorator class="hideOnMobile" name="header-small-banner"></gr-endpoint-decorator>
+        <gr-smart-search id="search" search-query="{{searchQuery}}"></gr-smart-search>
+        <gr-endpoint-decorator class="hideOnMobile" name="header-browse-source"></gr-endpoint-decorator>
         <div class="accountContainer" id="accountContainer">
-          <iron-icon id="mobileSearch" icon="gr-icons:search" on-tap='_onMobileSearchTap'></iron-icon>
-          <div class$="[[_computeIsInvisible(_registerURL)]]">
-            <a
-                class="registerButton"
-                href$="[[_registerURL]]">
+          <iron-icon id="mobileSearch" icon="gr-icons:search" on-tap="_onMobileSearchTap"></iron-icon>
+          <div class\$="[[_computeIsInvisible(_registerURL)]]">
+            <a class="registerButton" href\$="[[_registerURL]]">
               [[_registerText]]
             </a>
           </div>
-          <a class="loginButton" href$="[[loginUrl]]">Sign in</a>
-          <a
-              class="settingsButton"
-              href$="[[_generateSettingsLink()]]"
-              title="Settings">
+          <a class="loginButton" href\$="[[loginUrl]]">Sign in</a>
+          <a class="settingsButton" href\$="[[_generateSettingsLink()]]" title="Settings">
             <iron-icon icon="gr-icons:settings"></iron-icon>
           </a>
           <gr-account-dropdown account="[[_account]]"></gr-account-dropdown>
@@ -235,6 +207,4 @@
     </nav>
     <gr-js-api-interface id="jsAPI"></gr-js-api-interface>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
-  </template>
-  <script src="gr-main-header.js"></script>
-</dom-module>
+`;
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.html
index 8fe3fca..817643b 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.html
@@ -19,15 +19,20 @@
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-main-header</title>
 
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-main-header.html">
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
+<script type="module" src="../../../test/test-pre-setup.js"></script>
+<script type="module" src="../../../test/common-test-setup.js"></script>
+<script type="module" src="./gr-main-header.js"></script>
 
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-main-header.js';
+void(0);
+</script>
 
 <test-fixture id="basic">
   <template>
@@ -35,379 +40,381 @@
   </template>
 </test-fixture>
 
-<script>
-  suite('gr-main-header tests', async () => {
-    await readyToTest();
-    let element;
-    let sandbox;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-main-header.js';
+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); },
-      });
-      stub('gr-main-header', {
-        _loadAccount() {},
-      });
-      element = fixture('basic');
+  setup(() => {
+    sandbox = sinon.sandbox.create();
+    stub('gr-rest-api-interface', {
+      getConfig() { return Promise.resolve({}); },
+      probePath(path) { return Promise.resolve(false); },
     });
-
-    teardown(() => {
-      sandbox.restore();
+    stub('gr-main-header', {
+      _loadAccount() {},
     });
-
-    test('link visibility', () => {
-      element.loading = true;
-      assert.equal(getComputedStyle(element.shadowRoot
-          .querySelector('.accountContainer')).display,
-      'none');
-      element.loading = false;
-      element.loggedIn = false;
-      assert.notEqual(getComputedStyle(element.shadowRoot
-          .querySelector('.accountContainer')).display,
-      'none');
-      assert.notEqual(getComputedStyle(element.shadowRoot
-          .querySelector('.loginButton')).display,
-      'none');
-      assert.notEqual(getComputedStyle(element.shadowRoot
-          .querySelector('.registerButton')).display,
-      'none');
-      assert.equal(getComputedStyle(element.shadowRoot
-          .querySelector('gr-account-dropdown')).display,
-      'none');
-      assert.equal(getComputedStyle(element.shadowRoot
-          .querySelector('.settingsButton')).display,
-      'none');
-      element.loggedIn = true;
-      assert.equal(getComputedStyle(element.shadowRoot
-          .querySelector('.loginButton')).display,
-      'none');
-      assert.equal(getComputedStyle(element.shadowRoot
-          .querySelector('.registerButton')).display,
-      'none');
-      assert.notEqual(getComputedStyle(element.shadowRoot
-          .querySelector('gr-account-dropdown'))
-          .display,
-      'none');
-      assert.notEqual(getComputedStyle(element.shadowRoot
-          .querySelector('.settingsButton')).display,
-      'none');
-    });
-
-    test('fix my menu item', () => {
-      assert.deepEqual([
-        {url: 'https://awesometown.com/#hashyhash'},
-        {url: 'url', target: '_blank'},
-      ].map(element._fixCustomMenuItem), [
-        {url: 'https://awesometown.com/#hashyhash'},
-        {url: 'url'},
-      ]);
-    });
-
-    test('user links', () => {
-      const defaultLinks = [{
-        title: 'Faves',
-        links: [{
-          name: 'Pinterest',
-          url: 'https://pinterest.com',
-        }],
-      }];
-      const userLinks = [{
-        name: 'Facebook',
-        url: 'https://facebook.com',
-      }];
-      const adminLinks = [{
-        name: 'Repos',
-        url: '/repos',
-      }];
-
-      // When no admin links are passed, it should use the default.
-      assert.deepEqual(element._computeLinks(
-          defaultLinks,
-          /* userLinks= */[],
-          adminLinks,
-          /* topMenus= */[],
-          /* docBaseUrl= */ ''
-      ),
-      defaultLinks.concat({
-        title: 'Browse',
-        links: adminLinks,
-      }));
-      assert.deepEqual(element._computeLinks(
-          defaultLinks,
-          userLinks,
-          adminLinks,
-          /* topMenus= */[],
-          /* docBaseUrl= */ ''
-      ),
-      defaultLinks.concat([
-        {
-          title: 'Your',
-          links: userLinks,
-        },
-        {
-          title: 'Browse',
-          links: adminLinks,
-        }])
-      );
-    });
-
-    test('documentation links', () => {
-      const docLinks = [
-        {
-          name: 'Table of Contents',
-          url: '/index.html',
-        },
-      ];
-
-      assert.deepEqual(element._getDocLinks(null, docLinks), []);
-      assert.deepEqual(element._getDocLinks('', docLinks), []);
-      assert.deepEqual(element._getDocLinks('base', null), []);
-      assert.deepEqual(element._getDocLinks('base', []), []);
-
-      assert.deepEqual(element._getDocLinks('base', docLinks), [{
-        name: 'Table of Contents',
-        target: '_blank',
-        url: 'base/index.html',
-      }]);
-
-      assert.deepEqual(element._getDocLinks('base/', docLinks), [{
-        name: 'Table of Contents',
-        target: '_blank',
-        url: 'base/index.html',
-      }]);
-    });
-
-    test('top menus', () => {
-      const adminLinks = [{
-        name: 'Repos',
-        url: '/repos',
-      }];
-      const topMenus = [{
-        name: 'Plugins',
-        items: [{
-          name: 'Manage',
-          target: '_blank',
-          url: 'https://gerrit/plugins/plugin-manager/static/index.html',
-        }],
-      }];
-      assert.deepEqual(element._computeLinks(
-          /* defaultLinks= */ [],
-          /* userLinks= */ [],
-          adminLinks,
-          topMenus,
-          /* baseDocUrl= */ ''
-      ), [{
-        title: 'Browse',
-        links: adminLinks,
-      },
-      {
-        title: 'Plugins',
-        links: [{
-          name: 'Manage',
-          url: 'https://gerrit/plugins/plugin-manager/static/index.html',
-        }],
-      }]);
-    });
-
-    test('ignore top project menus', () => {
-      const adminLinks = [{
-        name: 'Repos',
-        url: '/repos',
-      }];
-      const topMenus = [{
-        name: 'Projects',
-        items: [{
-          name: 'Project Settings',
-          target: '_blank',
-          url: '/plugins/myplugin/${projectName}',
-        }, {
-          name: 'Project List',
-          target: '_blank',
-          url: '/plugins/myplugin/index.html',
-        }],
-      }];
-      assert.deepEqual(element._computeLinks(
-          /* defaultLinks= */ [],
-          /* userLinks= */ [],
-          adminLinks,
-          topMenus,
-          /* baseDocUrl= */ ''
-      ), [{
-        title: 'Browse',
-        links: adminLinks,
-      },
-      {
-        title: 'Projects',
-        links: [{
-          name: 'Project List',
-          url: '/plugins/myplugin/index.html',
-        }],
-      }]);
-    });
-
-    test('merge top menus', () => {
-      const adminLinks = [{
-        name: 'Repos',
-        url: '/repos',
-      }];
-      const topMenus = [{
-        name: 'Plugins',
-        items: [{
-          name: 'Manage',
-          target: '_blank',
-          url: 'https://gerrit/plugins/plugin-manager/static/index.html',
-        }],
-      }, {
-        name: 'Plugins',
-        items: [{
-          name: 'Create',
-          target: '_blank',
-          url: 'https://gerrit/plugins/plugin-manager/static/create.html',
-        }],
-      }];
-      assert.deepEqual(element._computeLinks(
-          /* defaultLinks= */ [],
-          /* userLinks= */ [],
-          adminLinks,
-          topMenus,
-          /* baseDocUrl= */ ''
-      ), [{
-        title: 'Browse',
-        links: adminLinks,
-      }, {
-        title: 'Plugins',
-        links: [{
-          name: 'Manage',
-          url: 'https://gerrit/plugins/plugin-manager/static/index.html',
-        }, {
-          name: 'Create',
-          url: 'https://gerrit/plugins/plugin-manager/static/create.html',
-        }],
-      }]);
-    });
-
-    test('merge top menus in default links', () => {
-      const defaultLinks = [{
-        title: 'Faves',
-        links: [{
-          name: 'Pinterest',
-          url: 'https://pinterest.com',
-        }],
-      }];
-      const topMenus = [{
-        name: 'Faves',
-        items: [{
-          name: 'Manage',
-          target: '_blank',
-          url: 'https://gerrit/plugins/plugin-manager/static/index.html',
-        }],
-      }];
-      assert.deepEqual(element._computeLinks(
-          defaultLinks,
-          /* userLinks= */ [],
-          /* adminLinks= */ [],
-          topMenus,
-          /* baseDocUrl= */ ''
-      ), [{
-        title: 'Faves',
-        links: defaultLinks[0].links.concat([{
-          name: 'Manage',
-          url: 'https://gerrit/plugins/plugin-manager/static/index.html',
-        }]),
-      }, {
-        title: 'Browse',
-        links: [],
-      }]);
-    });
-
-    test('merge top menus in user links', () => {
-      const userLinks = [{
-        name: 'Facebook',
-        url: 'https://facebook.com',
-      }];
-      const topMenus = [{
-        name: 'Your',
-        items: [{
-          name: 'Manage',
-          target: '_blank',
-          url: 'https://gerrit/plugins/plugin-manager/static/index.html',
-        }],
-      }];
-      assert.deepEqual(element._computeLinks(
-          /* defaultLinks= */ [],
-          userLinks,
-          /* adminLinks= */ [],
-          topMenus,
-          /* baseDocUrl= */ ''
-      ), [{
-        title: 'Your',
-        links: userLinks.concat([{
-          name: 'Manage',
-          url: 'https://gerrit/plugins/plugin-manager/static/index.html',
-        }]),
-      }, {
-        title: 'Browse',
-        links: [],
-      }]);
-    });
-
-    test('merge top menus in admin links', () => {
-      const adminLinks = [{
-        name: 'Repos',
-        url: '/repos',
-      }];
-      const topMenus = [{
-        name: 'Browse',
-        items: [{
-          name: 'Manage',
-          target: '_blank',
-          url: 'https://gerrit/plugins/plugin-manager/static/index.html',
-        }],
-      }];
-      assert.deepEqual(element._computeLinks(
-          /* defaultLinks= */ [],
-          /* userLinks= */ [],
-          adminLinks,
-          topMenus,
-          /* baseDocUrl= */ ''
-      ), [{
-        title: 'Browse',
-        links: adminLinks.concat([{
-          name: 'Manage',
-          url: 'https://gerrit/plugins/plugin-manager/static/index.html',
-        }]),
-      }]);
-    });
-
-    test('register URL', () => {
-      const config = {
-        auth: {
-          auth_type: 'LDAP',
-          register_url: 'https//gerrit.example.com/register',
-        },
-      };
-      element._retrieveRegisterURL(config);
-      assert.equal(element._registerURL, config.auth.register_url);
-      assert.equal(element._registerText, 'Sign up');
-
-      config.auth.register_text = 'Create account';
-      element._retrieveRegisterURL(config);
-      assert.equal(element._registerURL, config.auth.register_url);
-      assert.equal(element._registerText, config.auth.register_text);
-    });
-
-    test('register URL ignored for wrong auth type', () => {
-      const config = {
-        auth: {
-          auth_type: 'OPENID',
-          register_url: 'https//gerrit.example.com/register',
-        },
-      };
-      element._retrieveRegisterURL(config);
-      assert.equal(element._registerURL, null);
-      assert.equal(element._registerText, 'Sign up');
-    });
+    element = fixture('basic');
   });
-      </script>
+
+  teardown(() => {
+    sandbox.restore();
+  });
+
+  test('link visibility', () => {
+    element.loading = true;
+    assert.equal(getComputedStyle(element.shadowRoot
+        .querySelector('.accountContainer')).display,
+    'none');
+    element.loading = false;
+    element.loggedIn = false;
+    assert.notEqual(getComputedStyle(element.shadowRoot
+        .querySelector('.accountContainer')).display,
+    'none');
+    assert.notEqual(getComputedStyle(element.shadowRoot
+        .querySelector('.loginButton')).display,
+    'none');
+    assert.notEqual(getComputedStyle(element.shadowRoot
+        .querySelector('.registerButton')).display,
+    'none');
+    assert.equal(getComputedStyle(element.shadowRoot
+        .querySelector('gr-account-dropdown')).display,
+    'none');
+    assert.equal(getComputedStyle(element.shadowRoot
+        .querySelector('.settingsButton')).display,
+    'none');
+    element.loggedIn = true;
+    assert.equal(getComputedStyle(element.shadowRoot
+        .querySelector('.loginButton')).display,
+    'none');
+    assert.equal(getComputedStyle(element.shadowRoot
+        .querySelector('.registerButton')).display,
+    'none');
+    assert.notEqual(getComputedStyle(element.shadowRoot
+        .querySelector('gr-account-dropdown'))
+        .display,
+    'none');
+    assert.notEqual(getComputedStyle(element.shadowRoot
+        .querySelector('.settingsButton')).display,
+    'none');
+  });
+
+  test('fix my menu item', () => {
+    assert.deepEqual([
+      {url: 'https://awesometown.com/#hashyhash'},
+      {url: 'url', target: '_blank'},
+    ].map(element._fixCustomMenuItem), [
+      {url: 'https://awesometown.com/#hashyhash'},
+      {url: 'url'},
+    ]);
+  });
+
+  test('user links', () => {
+    const defaultLinks = [{
+      title: 'Faves',
+      links: [{
+        name: 'Pinterest',
+        url: 'https://pinterest.com',
+      }],
+    }];
+    const userLinks = [{
+      name: 'Facebook',
+      url: 'https://facebook.com',
+    }];
+    const adminLinks = [{
+      name: 'Repos',
+      url: '/repos',
+    }];
+
+    // When no admin links are passed, it should use the default.
+    assert.deepEqual(element._computeLinks(
+        defaultLinks,
+        /* userLinks= */[],
+        adminLinks,
+        /* topMenus= */[],
+        /* docBaseUrl= */ ''
+    ),
+    defaultLinks.concat({
+      title: 'Browse',
+      links: adminLinks,
+    }));
+    assert.deepEqual(element._computeLinks(
+        defaultLinks,
+        userLinks,
+        adminLinks,
+        /* topMenus= */[],
+        /* docBaseUrl= */ ''
+    ),
+    defaultLinks.concat([
+      {
+        title: 'Your',
+        links: userLinks,
+      },
+      {
+        title: 'Browse',
+        links: adminLinks,
+      }])
+    );
+  });
+
+  test('documentation links', () => {
+    const docLinks = [
+      {
+        name: 'Table of Contents',
+        url: '/index.html',
+      },
+    ];
+
+    assert.deepEqual(element._getDocLinks(null, docLinks), []);
+    assert.deepEqual(element._getDocLinks('', docLinks), []);
+    assert.deepEqual(element._getDocLinks('base', null), []);
+    assert.deepEqual(element._getDocLinks('base', []), []);
+
+    assert.deepEqual(element._getDocLinks('base', docLinks), [{
+      name: 'Table of Contents',
+      target: '_blank',
+      url: 'base/index.html',
+    }]);
+
+    assert.deepEqual(element._getDocLinks('base/', docLinks), [{
+      name: 'Table of Contents',
+      target: '_blank',
+      url: 'base/index.html',
+    }]);
+  });
+
+  test('top menus', () => {
+    const adminLinks = [{
+      name: 'Repos',
+      url: '/repos',
+    }];
+    const topMenus = [{
+      name: 'Plugins',
+      items: [{
+        name: 'Manage',
+        target: '_blank',
+        url: 'https://gerrit/plugins/plugin-manager/static/index.html',
+      }],
+    }];
+    assert.deepEqual(element._computeLinks(
+        /* defaultLinks= */ [],
+        /* userLinks= */ [],
+        adminLinks,
+        topMenus,
+        /* baseDocUrl= */ ''
+    ), [{
+      title: 'Browse',
+      links: adminLinks,
+    },
+    {
+      title: 'Plugins',
+      links: [{
+        name: 'Manage',
+        url: 'https://gerrit/plugins/plugin-manager/static/index.html',
+      }],
+    }]);
+  });
+
+  test('ignore top project menus', () => {
+    const adminLinks = [{
+      name: 'Repos',
+      url: '/repos',
+    }];
+    const topMenus = [{
+      name: 'Projects',
+      items: [{
+        name: 'Project Settings',
+        target: '_blank',
+        url: '/plugins/myplugin/${projectName}',
+      }, {
+        name: 'Project List',
+        target: '_blank',
+        url: '/plugins/myplugin/index.html',
+      }],
+    }];
+    assert.deepEqual(element._computeLinks(
+        /* defaultLinks= */ [],
+        /* userLinks= */ [],
+        adminLinks,
+        topMenus,
+        /* baseDocUrl= */ ''
+    ), [{
+      title: 'Browse',
+      links: adminLinks,
+    },
+    {
+      title: 'Projects',
+      links: [{
+        name: 'Project List',
+        url: '/plugins/myplugin/index.html',
+      }],
+    }]);
+  });
+
+  test('merge top menus', () => {
+    const adminLinks = [{
+      name: 'Repos',
+      url: '/repos',
+    }];
+    const topMenus = [{
+      name: 'Plugins',
+      items: [{
+        name: 'Manage',
+        target: '_blank',
+        url: 'https://gerrit/plugins/plugin-manager/static/index.html',
+      }],
+    }, {
+      name: 'Plugins',
+      items: [{
+        name: 'Create',
+        target: '_blank',
+        url: 'https://gerrit/plugins/plugin-manager/static/create.html',
+      }],
+    }];
+    assert.deepEqual(element._computeLinks(
+        /* defaultLinks= */ [],
+        /* userLinks= */ [],
+        adminLinks,
+        topMenus,
+        /* baseDocUrl= */ ''
+    ), [{
+      title: 'Browse',
+      links: adminLinks,
+    }, {
+      title: 'Plugins',
+      links: [{
+        name: 'Manage',
+        url: 'https://gerrit/plugins/plugin-manager/static/index.html',
+      }, {
+        name: 'Create',
+        url: 'https://gerrit/plugins/plugin-manager/static/create.html',
+      }],
+    }]);
+  });
+
+  test('merge top menus in default links', () => {
+    const defaultLinks = [{
+      title: 'Faves',
+      links: [{
+        name: 'Pinterest',
+        url: 'https://pinterest.com',
+      }],
+    }];
+    const topMenus = [{
+      name: 'Faves',
+      items: [{
+        name: 'Manage',
+        target: '_blank',
+        url: 'https://gerrit/plugins/plugin-manager/static/index.html',
+      }],
+    }];
+    assert.deepEqual(element._computeLinks(
+        defaultLinks,
+        /* userLinks= */ [],
+        /* adminLinks= */ [],
+        topMenus,
+        /* baseDocUrl= */ ''
+    ), [{
+      title: 'Faves',
+      links: defaultLinks[0].links.concat([{
+        name: 'Manage',
+        url: 'https://gerrit/plugins/plugin-manager/static/index.html',
+      }]),
+    }, {
+      title: 'Browse',
+      links: [],
+    }]);
+  });
+
+  test('merge top menus in user links', () => {
+    const userLinks = [{
+      name: 'Facebook',
+      url: 'https://facebook.com',
+    }];
+    const topMenus = [{
+      name: 'Your',
+      items: [{
+        name: 'Manage',
+        target: '_blank',
+        url: 'https://gerrit/plugins/plugin-manager/static/index.html',
+      }],
+    }];
+    assert.deepEqual(element._computeLinks(
+        /* defaultLinks= */ [],
+        userLinks,
+        /* adminLinks= */ [],
+        topMenus,
+        /* baseDocUrl= */ ''
+    ), [{
+      title: 'Your',
+      links: userLinks.concat([{
+        name: 'Manage',
+        url: 'https://gerrit/plugins/plugin-manager/static/index.html',
+      }]),
+    }, {
+      title: 'Browse',
+      links: [],
+    }]);
+  });
+
+  test('merge top menus in admin links', () => {
+    const adminLinks = [{
+      name: 'Repos',
+      url: '/repos',
+    }];
+    const topMenus = [{
+      name: 'Browse',
+      items: [{
+        name: 'Manage',
+        target: '_blank',
+        url: 'https://gerrit/plugins/plugin-manager/static/index.html',
+      }],
+    }];
+    assert.deepEqual(element._computeLinks(
+        /* defaultLinks= */ [],
+        /* userLinks= */ [],
+        adminLinks,
+        topMenus,
+        /* baseDocUrl= */ ''
+    ), [{
+      title: 'Browse',
+      links: adminLinks.concat([{
+        name: 'Manage',
+        url: 'https://gerrit/plugins/plugin-manager/static/index.html',
+      }]),
+    }]);
+  });
+
+  test('register URL', () => {
+    const config = {
+      auth: {
+        auth_type: 'LDAP',
+        register_url: 'https//gerrit.example.com/register',
+      },
+    };
+    element._retrieveRegisterURL(config);
+    assert.equal(element._registerURL, config.auth.register_url);
+    assert.equal(element._registerText, 'Sign up');
+
+    config.auth.register_text = 'Create account';
+    element._retrieveRegisterURL(config);
+    assert.equal(element._registerURL, config.auth.register_url);
+    assert.equal(element._registerText, config.auth.register_text);
+  });
+
+  test('register URL ignored for wrong auth type', () => {
+    const config = {
+      auth: {
+        auth_type: 'OPENID',
+        register_url: 'https//gerrit.example.com/register',
+      },
+    };
+    element._retrieveRegisterURL(config);
+    assert.equal(element._registerURL, null);
+    assert.equal(element._registerText, 'Sign up');
+  });
+});
+</script>
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.js b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.js
index e79277a..c8724c3 100644
--- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.js
+++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.js
@@ -1,750 +1,748 @@
-<!--
-@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.
+ */
+(function(window) {
+  'use strict';
 
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
+  // Navigation parameters object format:
+  //
+  // Each object has a `view` property with a value from Gerrit.Nav.View. The
+  // remaining properties depend on the value used for view.
+  //
+  //  - Gerrit.Nav.View.CHANGE:
+  //    - `changeNum`, required, String: the numeric ID of the change.
+  //    - `project`, optional, String: the project name.
+  //    - `patchNum`, optional, Number: the patch for the right-hand-side of
+  //        the diff.
+  //    - `basePatchNum`, optional, Number: the patch for the left-hand-side
+  //        of the diff. If `basePatchNum` is provided, then `patchNum` must
+  //        also be provided.
+  //    - `edit`, optional, Boolean: whether or not to load the file list with
+  //        edit controls.
+  //    - `messageHash`, optional, String: the hash of the change message to
+  //        scroll to.
+  //
+  // - Gerrit.Nav.View.SEARCH:
+  //    - `query`, optional, String: the literal search query. If provided,
+  //        the string will be used as the query, and all other params will be
+  //        ignored.
+  //    - `owner`, optional, String: the owner name.
+  //    - `project`, optional, String: the project name.
+  //    - `branch`, optional, String: the branch name.
+  //    - `topic`, optional, String: the topic name.
+  //    - `hashtag`, optional, String: the hashtag name.
+  //    - `statuses`, optional, Array<String>: the list of change statuses to
+  //        search for. If more than one is provided, the search will OR them
+  //        together.
+  //    - `offset`, optional, Number: the offset for the query.
+  //
+  //  - Gerrit.Nav.View.DIFF:
+  //    - `changeNum`, required, String: the numeric ID of the change.
+  //    - `path`, required, String: the filepath of the diff.
+  //    - `patchNum`, required, Number: the patch for the right-hand-side of
+  //        the diff.
+  //    - `basePatchNum`, optional, Number: the patch for the left-hand-side
+  //        of the diff. If `basePatchNum` is provided, then `patchNum` must
+  //        also be provided.
+  //    - `lineNum`, optional, Number: the line number to be selected on load.
+  //    - `leftSide`, optional, Boolean: if a `lineNum` is provided, a value
+  //        of true selects the line from base of the patch range. False by
+  //        default.
+  //
+  //  - Gerrit.Nav.View.GROUP:
+  //    - `groupId`, required, String: the ID of the group.
+  //    - `detail`, optional, String: the name of the group detail view.
+  //      Takes any value from Gerrit.Nav.GroupDetailView.
+  //
+  //  - Gerrit.Nav.View.REPO:
+  //    - `repoName`, required, String: the name of the repo
+  //    - `detail`, optional, String: the name of the repo detail view.
+  //      Takes any value from Gerrit.Nav.RepoDetailView.
+  //
+  //  - Gerrit.Nav.View.DASHBOARD
+  //    - `repo`, optional, String.
+  //    - `sections`, optional, Array of objects with `title` and `query`
+  //      strings.
+  //    - `user`, optional, String.
+  //
+  //  - Gerrit.Nav.View.ROOT:
+  //    - no possible parameters.
 
-http://www.apache.org/licenses/LICENSE-2.0
+  window.Gerrit = window.Gerrit || {};
 
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<script>
-  (function(window) {
-    'use strict';
+  // Prevent redefinition.
+  if (window.Gerrit.hasOwnProperty('Nav')) { return; }
 
-    // Navigation parameters object format:
-    //
-    // Each object has a `view` property with a value from Gerrit.Nav.View. The
-    // remaining properties depend on the value used for view.
-    //
-    //  - Gerrit.Nav.View.CHANGE:
-    //    - `changeNum`, required, String: the numeric ID of the change.
-    //    - `project`, optional, String: the project name.
-    //    - `patchNum`, optional, Number: the patch for the right-hand-side of
-    //        the diff.
-    //    - `basePatchNum`, optional, Number: the patch for the left-hand-side
-    //        of the diff. If `basePatchNum` is provided, then `patchNum` must
-    //        also be provided.
-    //    - `edit`, optional, Boolean: whether or not to load the file list with
-    //        edit controls.
-    //    - `messageHash`, optional, String: the hash of the change message to
-    //        scroll to.
-    //
-    // - Gerrit.Nav.View.SEARCH:
-    //    - `query`, optional, String: the literal search query. If provided,
-    //        the string will be used as the query, and all other params will be
-    //        ignored.
-    //    - `owner`, optional, String: the owner name.
-    //    - `project`, optional, String: the project name.
-    //    - `branch`, optional, String: the branch name.
-    //    - `topic`, optional, String: the topic name.
-    //    - `hashtag`, optional, String: the hashtag name.
-    //    - `statuses`, optional, Array<String>: the list of change statuses to
-    //        search for. If more than one is provided, the search will OR them
-    //        together.
-    //    - `offset`, optional, Number: the offset for the query.
-    //
-    //  - Gerrit.Nav.View.DIFF:
-    //    - `changeNum`, required, String: the numeric ID of the change.
-    //    - `path`, required, String: the filepath of the diff.
-    //    - `patchNum`, required, Number: the patch for the right-hand-side of
-    //        the diff.
-    //    - `basePatchNum`, optional, Number: the patch for the left-hand-side
-    //        of the diff. If `basePatchNum` is provided, then `patchNum` must
-    //        also be provided.
-    //    - `lineNum`, optional, Number: the line number to be selected on load.
-    //    - `leftSide`, optional, Boolean: if a `lineNum` is provided, a value
-    //        of true selects the line from base of the patch range. False by
-    //        default.
-    //
-    //  - Gerrit.Nav.View.GROUP:
-    //    - `groupId`, required, String: the ID of the group.
-    //    - `detail`, optional, String: the name of the group detail view.
-    //      Takes any value from Gerrit.Nav.GroupDetailView.
-    //
-    //  - Gerrit.Nav.View.REPO:
-    //    - `repoName`, required, String: the name of the repo
-    //    - `detail`, optional, String: the name of the repo detail view.
-    //      Takes any value from Gerrit.Nav.RepoDetailView.
-    //
-    //  - Gerrit.Nav.View.DASHBOARD
-    //    - `repo`, optional, String.
-    //    - `sections`, optional, Array of objects with `title` and `query`
-    //      strings.
-    //    - `user`, optional, String.
-    //
-    //  - Gerrit.Nav.View.ROOT:
-    //    - no possible parameters.
+  const uninitialized = () => {
+    console.warn('Use of uninitialized routing');
+  };
 
-    window.Gerrit = window.Gerrit || {};
+  const EDIT_PATCHNUM = 'edit';
+  const PARENT_PATCHNUM = 'PARENT';
 
-    // Prevent redefinition.
-    if (window.Gerrit.hasOwnProperty('Nav')) { return; }
+  const USER_PLACEHOLDER_PATTERN = /\$\{user\}/g;
 
-    const uninitialized = () => {
-      console.warn('Use of uninitialized routing');
-    };
+  // NOTE: These queries are tested in Java. Any changes made to definitions
+  // here require corresponding changes to:
+  // javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+  const DEFAULT_SECTIONS = [
+    {
+      // Changes with unpublished draft comments. This section is omitted when
+      // viewing other users, so we don't need to filter anything out.
+      name: 'Has draft comments',
+      query: 'has:draft',
+      selfOnly: true,
+      hideIfEmpty: true,
+      suffixForDashboard: 'limit:10',
+    },
+    {
+      // Changes that are assigned to the viewed user.
+      name: 'Assigned reviews',
+      query: 'assignee:${user} (-is:wip OR owner:self OR assignee:self) ' +
+          'is:open -is:ignored',
+      hideIfEmpty: true,
+      suffixForDashboard: 'limit:25',
+    },
+    {
+      // WIP open changes owned by viewing user. This section is omitted when
+      // viewing other users, so we don't need to filter anything out.
+      name: 'Work in progress',
+      query: 'is:open owner:${user} is:wip',
+      selfOnly: true,
+      hideIfEmpty: true,
+      suffixForDashboard: 'limit:25',
+    },
+    {
+      // Non-WIP open changes owned by viewed user. Filter out changes ignored
+      // by the viewing user.
+      name: 'Outgoing reviews',
+      query: 'is:open owner:${user} -is:wip -is:ignored',
+      isOutgoing: true,
+      suffixForDashboard: 'limit:25',
+    },
+    {
+      // Non-WIP open changes not owned by the viewed user, that the viewed user
+      // is associated with (as either a reviewer or the assignee). Changes
+      // ignored by the viewing user are filtered out.
+      name: 'Incoming reviews',
+      query: 'is:open -owner:${user} -is:wip -is:ignored ' +
+          '(reviewer:${user} OR assignee:${user})',
+      suffixForDashboard: 'limit:25',
+    },
+    {
+      // Open changes the viewed user is CCed on. Changes ignored by the viewing
+      // user are filtered out.
+      name: 'CCed on',
+      query: 'is:open -is:ignored cc:${user}',
+      suffixForDashboard: 'limit:10',
+    },
+    {
+      name: 'Recently closed',
+      // Closed changes where viewed user is owner, reviewer, or assignee.
+      // Changes ignored by the viewing user are filtered out, and so are WIP
+      // changes not owned by the viewing user (the one instance of
+      // 'owner:self' is intentional and implements this logic).
+      query: 'is:closed -is:ignored (-is:wip OR owner:self) ' +
+          '(owner:${user} OR reviewer:${user} OR assignee:${user} ' +
+          'OR cc:${user})',
+      suffixForDashboard: '-age:4w limit:10',
+    },
+  ];
 
-    const EDIT_PATCHNUM = 'edit';
-    const PARENT_PATCHNUM = 'PARENT';
+  window.Gerrit.Nav = {
 
-    const USER_PLACEHOLDER_PATTERN = /\$\{user\}/g;
+    View: {
+      ADMIN: 'admin',
+      AGREEMENTS: 'agreements',
+      CHANGE: 'change',
+      DASHBOARD: 'dashboard',
+      DIFF: 'diff',
+      DOCUMENTATION_SEARCH: 'documentation-search',
+      EDIT: 'edit',
+      GROUP: 'group',
+      PLUGIN_SCREEN: 'plugin-screen',
+      REPO: 'repo',
+      ROOT: 'root',
+      SEARCH: 'search',
+      SETTINGS: 'settings',
+    },
 
-    // NOTE: These queries are tested in Java. Any changes made to definitions
-    // here require corresponding changes to:
-    // javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
-    const DEFAULT_SECTIONS = [
-      {
-        // Changes with unpublished draft comments. This section is omitted when
-        // viewing other users, so we don't need to filter anything out.
-        name: 'Has draft comments',
-        query: 'has:draft',
-        selfOnly: true,
-        hideIfEmpty: true,
-        suffixForDashboard: 'limit:10',
-      },
-      {
-        // Changes that are assigned to the viewed user.
-        name: 'Assigned reviews',
-        query: 'assignee:${user} (-is:wip OR owner:self OR assignee:self) ' +
-            'is:open -is:ignored',
-        hideIfEmpty: true,
-        suffixForDashboard: 'limit:25',
-      },
-      {
-        // WIP open changes owned by viewing user. This section is omitted when
-        // viewing other users, so we don't need to filter anything out.
-        name: 'Work in progress',
-        query: 'is:open owner:${user} is:wip',
-        selfOnly: true,
-        hideIfEmpty: true,
-        suffixForDashboard: 'limit:25',
-      },
-      {
-        // Non-WIP open changes owned by viewed user. Filter out changes ignored
-        // by the viewing user.
-        name: 'Outgoing reviews',
-        query: 'is:open owner:${user} -is:wip -is:ignored',
-        isOutgoing: true,
-        suffixForDashboard: 'limit:25',
-      },
-      {
-        // Non-WIP open changes not owned by the viewed user, that the viewed user
-        // is associated with (as either a reviewer or the assignee). Changes
-        // ignored by the viewing user are filtered out.
-        name: 'Incoming reviews',
-        query: 'is:open -owner:${user} -is:wip -is:ignored ' +
-            '(reviewer:${user} OR assignee:${user})',
-        suffixForDashboard: 'limit:25',
-      },
-      {
-        // Open changes the viewed user is CCed on. Changes ignored by the viewing
-        // user are filtered out.
-        name: 'CCed on',
-        query: 'is:open -is:ignored cc:${user}',
-        suffixForDashboard: 'limit:10',
-      },
-      {
-        name: 'Recently closed',
-        // Closed changes where viewed user is owner, reviewer, or assignee.
-        // Changes ignored by the viewing user are filtered out, and so are WIP
-        // changes not owned by the viewing user (the one instance of
-        // 'owner:self' is intentional and implements this logic).
-        query: 'is:closed -is:ignored (-is:wip OR owner:self) ' +
-            '(owner:${user} OR reviewer:${user} OR assignee:${user} ' +
-            'OR cc:${user})',
-        suffixForDashboard: '-age:4w limit:10',
-      },
-    ];
+    GroupDetailView: {
+      MEMBERS: 'members',
+      LOG: 'log',
+    },
 
-    window.Gerrit.Nav = {
+    RepoDetailView: {
+      ACCESS: 'access',
+      BRANCHES: 'branches',
+      COMMANDS: 'commands',
+      DASHBOARDS: 'dashboards',
+      TAGS: 'tags',
+    },
 
-      View: {
-        ADMIN: 'admin',
-        AGREEMENTS: 'agreements',
-        CHANGE: 'change',
-        DASHBOARD: 'dashboard',
-        DIFF: 'diff',
-        DOCUMENTATION_SEARCH: 'documentation-search',
-        EDIT: 'edit',
-        GROUP: 'group',
-        PLUGIN_SCREEN: 'plugin-screen',
-        REPO: 'repo',
-        ROOT: 'root',
-        SEARCH: 'search',
-        SETTINGS: 'settings',
-      },
+    WeblinkType: {
+      CHANGE: 'change',
+      FILE: 'file',
+      PATCHSET: 'patchset',
+    },
 
-      GroupDetailView: {
-        MEMBERS: 'members',
-        LOG: 'log',
-      },
+    /** @type {Function} */
+    _navigate: uninitialized,
 
-      RepoDetailView: {
-        ACCESS: 'access',
-        BRANCHES: 'branches',
-        COMMANDS: 'commands',
-        DASHBOARDS: 'dashboards',
-        TAGS: 'tags',
-      },
+    /** @type {Function} */
+    _generateUrl: uninitialized,
 
-      WeblinkType: {
-        CHANGE: 'change',
-        FILE: 'file',
-        PATCHSET: 'patchset',
-      },
+    /** @type {Function} */
+    _generateWeblinks: uninitialized,
 
-      /** @type {Function} */
-      _navigate: uninitialized,
+    /** @type {Function} */
+    mapCommentlinks: uninitialized,
 
-      /** @type {Function} */
-      _generateUrl: uninitialized,
+    /**
+     * @param {number=} patchNum
+     * @param {number|string=} basePatchNum
+     */
+    _checkPatchRange(patchNum, basePatchNum) {
+      if (basePatchNum && !patchNum) {
+        throw new Error('Cannot use base patch number without patch number.');
+      }
+    },
 
-      /** @type {Function} */
-      _generateWeblinks: uninitialized,
+    /**
+     * Setup router implementation.
+     *
+     * @param {function(!string)} navigate the router-abstracted equivalent of
+     *     `window.location.href = ...`. Takes a string.
+     * @param {function(!Object): string} generateUrl generates a URL given
+     *     navigation parameters, detailed in the file header.
+     * @param {function(!Object): string} generateWeblinks weblinks generator
+     *     function takes single payload parameter with type property that
+     *  determines which
+     *     part of the UI is the consumer of the weblinks. type property can
+     *     be one of file, change, or patchset.
+     *     - For file type, payload will also contain string properties: repo,
+     *         commit, file.
+     *     - For patchset type, payload will also contain string properties:
+     *         repo, commit.
+     *     - For change type, payload will also contain string properties:
+     *         repo, commit. If server provides weblinks, those will be passed
+     *         as options.weblinks property on the main payload object.
+     * @param {function(!Object): Object} mapCommentlinks provides an escape
+     *     hatch to modify the commentlinks object, e.g. if it contains any
+     *     relative URLs.
+     */
+    setup(navigate, generateUrl, generateWeblinks, mapCommentlinks) {
+      this._navigate = navigate;
+      this._generateUrl = generateUrl;
+      this._generateWeblinks = generateWeblinks;
+      this.mapCommentlinks = mapCommentlinks;
+    },
 
-      /** @type {Function} */
-      mapCommentlinks: uninitialized,
+    destroy() {
+      this._navigate = uninitialized;
+      this._generateUrl = uninitialized;
+      this._generateWeblinks = uninitialized;
+      this.mapCommentlinks = uninitialized;
+    },
 
-      /**
-       * @param {number=} patchNum
-       * @param {number|string=} basePatchNum
-       */
-      _checkPatchRange(patchNum, basePatchNum) {
-        if (basePatchNum && !patchNum) {
-          throw new Error('Cannot use base patch number without patch number.');
-        }
-      },
+    /**
+     * Generate a URL for the given route parameters.
+     *
+     * @param {Object} params
+     * @return {string}
+     */
+    _getUrlFor(params) {
+      return this._generateUrl(params);
+    },
 
-      /**
-       * Setup router implementation.
-       *
-       * @param {function(!string)} navigate the router-abstracted equivalent of
-       *     `window.location.href = ...`. Takes a string.
-       * @param {function(!Object): string} generateUrl generates a URL given
-       *     navigation parameters, detailed in the file header.
-       * @param {function(!Object): string} generateWeblinks weblinks generator
-       *     function takes single payload parameter with type property that
-       *  determines which
-       *     part of the UI is the consumer of the weblinks. type property can
-       *     be one of file, change, or patchset.
-       *     - For file type, payload will also contain string properties: repo,
-       *         commit, file.
-       *     - For patchset type, payload will also contain string properties:
-       *         repo, commit.
-       *     - For change type, payload will also contain string properties:
-       *         repo, commit. If server provides weblinks, those will be passed
-       *         as options.weblinks property on the main payload object.
-       * @param {function(!Object): Object} mapCommentlinks provides an escape
-       *     hatch to modify the commentlinks object, e.g. if it contains any
-       *     relative URLs.
-       */
-      setup(navigate, generateUrl, generateWeblinks, mapCommentlinks) {
-        this._navigate = navigate;
-        this._generateUrl = generateUrl;
-        this._generateWeblinks = generateWeblinks;
-        this.mapCommentlinks = mapCommentlinks;
-      },
+    getUrlForSearchQuery(query, opt_offset) {
+      return this._getUrlFor({
+        view: Gerrit.Nav.View.SEARCH,
+        query,
+        offset: opt_offset,
+      });
+    },
 
-      destroy() {
-        this._navigate = uninitialized;
-        this._generateUrl = uninitialized;
-        this._generateWeblinks = uninitialized;
-        this.mapCommentlinks = uninitialized;
-      },
+    /**
+     * @param {!string} project The name of the project.
+     * @param {boolean=} opt_openOnly When true, only search open changes in
+     *     the project.
+     * @param {string=} opt_host The host in which to search.
+     * @return {string}
+     */
+    getUrlForProjectChanges(project, opt_openOnly, opt_host) {
+      return this._getUrlFor({
+        view: Gerrit.Nav.View.SEARCH,
+        project,
+        statuses: opt_openOnly ? ['open'] : [],
+        host: opt_host,
+      });
+    },
 
-      /**
-       * Generate a URL for the given route parameters.
-       *
-       * @param {Object} params
-       * @return {string}
-       */
-      _getUrlFor(params) {
-        return this._generateUrl(params);
-      },
+    /**
+     * @param {string} branch The name of the branch.
+     * @param {string} project The name of the project.
+     * @param {string=} opt_status The status to search.
+     * @param {string=} opt_host The host in which to search.
+     * @return {string}
+     */
+    getUrlForBranch(branch, project, opt_status, opt_host) {
+      return this._getUrlFor({
+        view: Gerrit.Nav.View.SEARCH,
+        branch,
+        project,
+        statuses: opt_status ? [opt_status] : undefined,
+        host: opt_host,
+      });
+    },
 
-      getUrlForSearchQuery(query, opt_offset) {
-        return this._getUrlFor({
-          view: Gerrit.Nav.View.SEARCH,
-          query,
-          offset: opt_offset,
-        });
-      },
+    /**
+     * @param {string} topic The name of the topic.
+     * @param {string=} opt_host The host in which to search.
+     * @return {string}
+     */
+    getUrlForTopic(topic, opt_host) {
+      return this._getUrlFor({
+        view: Gerrit.Nav.View.SEARCH,
+        topic,
+        statuses: ['open', 'merged'],
+        host: opt_host,
+      });
+    },
 
-      /**
-       * @param {!string} project The name of the project.
-       * @param {boolean=} opt_openOnly When true, only search open changes in
-       *     the project.
-       * @param {string=} opt_host The host in which to search.
-       * @return {string}
-       */
-      getUrlForProjectChanges(project, opt_openOnly, opt_host) {
-        return this._getUrlFor({
-          view: Gerrit.Nav.View.SEARCH,
-          project,
-          statuses: opt_openOnly ? ['open'] : [],
-          host: opt_host,
-        });
-      },
+    /**
+     * @param {string} hashtag The name of the hashtag.
+     * @return {string}
+     */
+    getUrlForHashtag(hashtag) {
+      return this._getUrlFor({
+        view: Gerrit.Nav.View.SEARCH,
+        hashtag,
+        statuses: ['open', 'merged'],
+      });
+    },
 
-      /**
-       * @param {string} branch The name of the branch.
-       * @param {string} project The name of the project.
-       * @param {string=} opt_status The status to search.
-       * @param {string=} opt_host The host in which to search.
-       * @return {string}
-       */
-      getUrlForBranch(branch, project, opt_status, opt_host) {
-        return this._getUrlFor({
-          view: Gerrit.Nav.View.SEARCH,
-          branch,
-          project,
-          statuses: opt_status ? [opt_status] : undefined,
-          host: opt_host,
-        });
-      },
+    /**
+     * Navigate to a search for changes with the given status.
+     *
+     * @param {string} status
+     */
+    navigateToStatusSearch(status) {
+      this._navigate(this._getUrlFor({
+        view: Gerrit.Nav.View.SEARCH,
+        statuses: [status],
+      }));
+    },
 
-      /**
-       * @param {string} topic The name of the topic.
-       * @param {string=} opt_host The host in which to search.
-       * @return {string}
-       */
-      getUrlForTopic(topic, opt_host) {
-        return this._getUrlFor({
-          view: Gerrit.Nav.View.SEARCH,
-          topic,
-          statuses: ['open', 'merged'],
-          host: opt_host,
-        });
-      },
+    /**
+     * Navigate to a search query
+     *
+     * @param {string} query
+     * @param {number=} opt_offset
+     */
+    navigateToSearchQuery(query, opt_offset) {
+      return this._navigate(this.getUrlForSearchQuery(query, opt_offset));
+    },
 
-      /**
-       * @param {string} hashtag The name of the hashtag.
-       * @return {string}
-       */
-      getUrlForHashtag(hashtag) {
-        return this._getUrlFor({
-          view: Gerrit.Nav.View.SEARCH,
-          hashtag,
-          statuses: ['open', 'merged'],
-        });
-      },
+    /**
+     * Navigate to the user's dashboard
+     */
+    navigateToUserDashboard() {
+      return this._navigate(this.getUrlForUserDashboard('self'));
+    },
 
-      /**
-       * Navigate to a search for changes with the given status.
-       *
-       * @param {string} status
-       */
-      navigateToStatusSearch(status) {
-        this._navigate(this._getUrlFor({
-          view: Gerrit.Nav.View.SEARCH,
-          statuses: [status],
-        }));
-      },
+    /**
+     * @param {!Object} change The change object.
+     * @param {number=} opt_patchNum
+     * @param {number|string=} opt_basePatchNum The string 'PARENT' can be
+     *     used for none.
+     * @param {boolean=} opt_isEdit
+     * @param {string=} opt_messageHash
+     * @return {string}
+     */
+    getUrlForChange(change, opt_patchNum, opt_basePatchNum, opt_isEdit,
+        opt_messageHash) {
+      if (opt_basePatchNum === PARENT_PATCHNUM) {
+        opt_basePatchNum = undefined;
+      }
 
-      /**
-       * Navigate to a search query
-       *
-       * @param {string} query
-       * @param {number=} opt_offset
-       */
-      navigateToSearchQuery(query, opt_offset) {
-        return this._navigate(this.getUrlForSearchQuery(query, opt_offset));
-      },
+      this._checkPatchRange(opt_patchNum, opt_basePatchNum);
+      return this._getUrlFor({
+        view: Gerrit.Nav.View.CHANGE,
+        changeNum: change._number,
+        project: change.project,
+        patchNum: opt_patchNum,
+        basePatchNum: opt_basePatchNum,
+        edit: opt_isEdit,
+        host: change.internalHost || undefined,
+        messageHash: opt_messageHash,
+      });
+    },
 
-      /**
-       * Navigate to the user's dashboard
-       */
-      navigateToUserDashboard() {
-        return this._navigate(this.getUrlForUserDashboard('self'));
-      },
+    /**
+     * @param {number} changeNum
+     * @param {string} project The name of the project.
+     * @param {number=} opt_patchNum
+     * @return {string}
+     */
+    getUrlForChangeById(changeNum, project, opt_patchNum) {
+      return this._getUrlFor({
+        view: Gerrit.Nav.View.CHANGE,
+        changeNum,
+        project,
+        patchNum: opt_patchNum,
+      });
+    },
 
-      /**
-       * @param {!Object} change The change object.
-       * @param {number=} opt_patchNum
-       * @param {number|string=} opt_basePatchNum The string 'PARENT' can be
-       *     used for none.
-       * @param {boolean=} opt_isEdit
-       * @param {string=} opt_messageHash
-       * @return {string}
-       */
-      getUrlForChange(change, opt_patchNum, opt_basePatchNum, opt_isEdit,
-          opt_messageHash) {
-        if (opt_basePatchNum === PARENT_PATCHNUM) {
-          opt_basePatchNum = undefined;
-        }
+    /**
+     * @param {!Object} change The change object.
+     * @param {number=} opt_patchNum
+     * @param {number|string=} opt_basePatchNum The string 'PARENT' can be
+     *     used for none.
+     * @param {boolean=} opt_isEdit
+     */
+    navigateToChange(change, opt_patchNum, opt_basePatchNum, opt_isEdit) {
+      this._navigate(this.getUrlForChange(change, opt_patchNum,
+          opt_basePatchNum, opt_isEdit));
+    },
 
-        this._checkPatchRange(opt_patchNum, opt_basePatchNum);
-        return this._getUrlFor({
-          view: Gerrit.Nav.View.CHANGE,
-          changeNum: change._number,
-          project: change.project,
-          patchNum: opt_patchNum,
-          basePatchNum: opt_basePatchNum,
-          edit: opt_isEdit,
-          host: change.internalHost || undefined,
-          messageHash: opt_messageHash,
-        });
-      },
+    /**
+     * @param {{ _number: number, project: string }} change The change object.
+     * @param {string} path The file path.
+     * @param {number=} opt_patchNum
+     * @param {number|string=} opt_basePatchNum The string 'PARENT' can be
+     *     used for none.
+     * @param {number|string=} opt_lineNum
+     * @return {string}
+     */
+    getUrlForDiff(change, path, opt_patchNum, opt_basePatchNum, opt_lineNum) {
+      return this.getUrlForDiffById(change._number, change.project, path,
+          opt_patchNum, opt_basePatchNum, opt_lineNum);
+    },
 
-      /**
-       * @param {number} changeNum
-       * @param {string} project The name of the project.
-       * @param {number=} opt_patchNum
-       * @return {string}
-       */
-      getUrlForChangeById(changeNum, project, opt_patchNum) {
-        return this._getUrlFor({
-          view: Gerrit.Nav.View.CHANGE,
-          changeNum,
-          project,
-          patchNum: opt_patchNum,
-        });
-      },
+    /**
+     * @param {number} changeNum
+     * @param {string} project The name of the project.
+     * @param {string} path The file path.
+     * @param {number=} opt_patchNum
+     * @param {number|string=} opt_basePatchNum The string 'PARENT' can be
+     *     used for none.
+     * @param {number=} opt_lineNum
+     * @param {boolean=} opt_leftSide
+     * @return {string}
+     */
+    getUrlForDiffById(changeNum, project, path, opt_patchNum,
+        opt_basePatchNum, opt_lineNum, opt_leftSide) {
+      if (opt_basePatchNum === PARENT_PATCHNUM) {
+        opt_basePatchNum = undefined;
+      }
 
-      /**
-       * @param {!Object} change The change object.
-       * @param {number=} opt_patchNum
-       * @param {number|string=} opt_basePatchNum The string 'PARENT' can be
-       *     used for none.
-       * @param {boolean=} opt_isEdit
-       */
-      navigateToChange(change, opt_patchNum, opt_basePatchNum, opt_isEdit) {
-        this._navigate(this.getUrlForChange(change, opt_patchNum,
-            opt_basePatchNum, opt_isEdit));
-      },
+      this._checkPatchRange(opt_patchNum, opt_basePatchNum);
+      return this._getUrlFor({
+        view: Gerrit.Nav.View.DIFF,
+        changeNum,
+        project,
+        path,
+        patchNum: opt_patchNum,
+        basePatchNum: opt_basePatchNum,
+        lineNum: opt_lineNum,
+        leftSide: opt_leftSide,
+      });
+    },
 
-      /**
-       * @param {{ _number: number, project: string }} change The change object.
-       * @param {string} path The file path.
-       * @param {number=} opt_patchNum
-       * @param {number|string=} opt_basePatchNum The string 'PARENT' can be
-       *     used for none.
-       * @param {number|string=} opt_lineNum
-       * @return {string}
-       */
-      getUrlForDiff(change, path, opt_patchNum, opt_basePatchNum, opt_lineNum) {
-        return this.getUrlForDiffById(change._number, change.project, path,
-            opt_patchNum, opt_basePatchNum, opt_lineNum);
-      },
+    /**
+     * @param {{ _number: number, project: string }} change The change object.
+     * @param {string} path The file path.
+     * @param {number=} opt_patchNum
+     * @return {string}
+     */
+    getEditUrlForDiff(change, path, opt_patchNum) {
+      return this.getEditUrlForDiffById(change._number, change.project, path,
+          opt_patchNum);
+    },
 
-      /**
-       * @param {number} changeNum
-       * @param {string} project The name of the project.
-       * @param {string} path The file path.
-       * @param {number=} opt_patchNum
-       * @param {number|string=} opt_basePatchNum The string 'PARENT' can be
-       *     used for none.
-       * @param {number=} opt_lineNum
-       * @param {boolean=} opt_leftSide
-       * @return {string}
-       */
-      getUrlForDiffById(changeNum, project, path, opt_patchNum,
-          opt_basePatchNum, opt_lineNum, opt_leftSide) {
-        if (opt_basePatchNum === PARENT_PATCHNUM) {
-          opt_basePatchNum = undefined;
-        }
+    /**
+     * @param {number} changeNum
+     * @param {string} project The name of the project.
+     * @param {string} path The file path.
+     * @param {number|string=} opt_patchNum The patchNum the file content
+     *    should be based on, or ${EDIT_PATCHNUM} if left undefined.
+     * @return {string}
+     */
+    getEditUrlForDiffById(changeNum, project, path, opt_patchNum) {
+      return this._getUrlFor({
+        view: Gerrit.Nav.View.EDIT,
+        changeNum,
+        project,
+        path,
+        patchNum: opt_patchNum || EDIT_PATCHNUM,
+      });
+    },
 
-        this._checkPatchRange(opt_patchNum, opt_basePatchNum);
-        return this._getUrlFor({
-          view: Gerrit.Nav.View.DIFF,
-          changeNum,
-          project,
-          path,
-          patchNum: opt_patchNum,
-          basePatchNum: opt_basePatchNum,
-          lineNum: opt_lineNum,
-          leftSide: opt_leftSide,
-        });
-      },
+    /**
+     * @param {!Object} change The change object.
+     * @param {string} path The file path.
+     * @param {number=} opt_patchNum
+     * @param {number|string=} opt_basePatchNum The string 'PARENT' can be
+     *     used for none.
+     */
+    navigateToDiff(change, path, opt_patchNum, opt_basePatchNum) {
+      this._navigate(this.getUrlForDiff(change, path, opt_patchNum,
+          opt_basePatchNum));
+    },
 
-      /**
-       * @param {{ _number: number, project: string }} change The change object.
-       * @param {string} path The file path.
-       * @param {number=} opt_patchNum
-       * @return {string}
-       */
-      getEditUrlForDiff(change, path, opt_patchNum) {
-        return this.getEditUrlForDiffById(change._number, change.project, path,
-            opt_patchNum);
-      },
+    /**
+     * @param {string} owner The name of the owner.
+     * @return {string}
+     */
+    getUrlForOwner(owner) {
+      return this._getUrlFor({
+        view: Gerrit.Nav.View.SEARCH,
+        owner,
+      });
+    },
 
-      /**
-       * @param {number} changeNum
-       * @param {string} project The name of the project.
-       * @param {string} path The file path.
-       * @param {number|string=} opt_patchNum The patchNum the file content
-       *    should be based on, or ${EDIT_PATCHNUM} if left undefined.
-       * @return {string}
-       */
-      getEditUrlForDiffById(changeNum, project, path, opt_patchNum) {
-        return this._getUrlFor({
-          view: Gerrit.Nav.View.EDIT,
-          changeNum,
-          project,
-          path,
-          patchNum: opt_patchNum || EDIT_PATCHNUM,
-        });
-      },
+    /**
+     * @param {string} user The name of the user.
+     * @return {string}
+     */
+    getUrlForUserDashboard(user) {
+      return this._getUrlFor({
+        view: Gerrit.Nav.View.DASHBOARD,
+        user,
+      });
+    },
 
-      /**
-       * @param {!Object} change The change object.
-       * @param {string} path The file path.
-       * @param {number=} opt_patchNum
-       * @param {number|string=} opt_basePatchNum The string 'PARENT' can be
-       *     used for none.
-       */
-      navigateToDiff(change, path, opt_patchNum, opt_basePatchNum) {
-        this._navigate(this.getUrlForDiff(change, path, opt_patchNum,
-            opt_basePatchNum));
-      },
+    /**
+     * @return {string}
+     */
+    getUrlForRoot() {
+      return this._getUrlFor({
+        view: Gerrit.Nav.View.ROOT,
+      });
+    },
 
-      /**
-       * @param {string} owner The name of the owner.
-       * @return {string}
-       */
-      getUrlForOwner(owner) {
-        return this._getUrlFor({
-          view: Gerrit.Nav.View.SEARCH,
-          owner,
-        });
-      },
+    /**
+     * @param {string} repo The name of the repo.
+     * @param {string} dashboard The ID of the dashboard, in the form of
+     *     '<ref>:<path>'.
+     * @return {string}
+     */
+    getUrlForRepoDashboard(repo, dashboard) {
+      return this._getUrlFor({
+        view: Gerrit.Nav.View.DASHBOARD,
+        repo,
+        dashboard,
+      });
+    },
 
-      /**
-       * @param {string} user The name of the user.
-       * @return {string}
-       */
-      getUrlForUserDashboard(user) {
-        return this._getUrlFor({
-          view: Gerrit.Nav.View.DASHBOARD,
-          user,
-        });
-      },
+    /**
+     * Navigate to an arbitrary relative URL.
+     *
+     * @param {string} relativeUrl
+     */
+    navigateToRelativeUrl(relativeUrl) {
+      if (!relativeUrl.startsWith('/')) {
+        throw new Error('navigateToRelativeUrl with non-relative URL');
+      }
+      this._navigate(relativeUrl);
+    },
 
-      /**
-       * @return {string}
-       */
-      getUrlForRoot() {
-        return this._getUrlFor({
-          view: Gerrit.Nav.View.ROOT,
-        });
-      },
+    /**
+     * @param {string} repoName
+     * @return {string}
+     */
+    getUrlForRepo(repoName) {
+      return this._getUrlFor({
+        view: Gerrit.Nav.View.REPO,
+        repoName,
+      });
+    },
 
-      /**
-       * @param {string} repo The name of the repo.
-       * @param {string} dashboard The ID of the dashboard, in the form of
-       *     '<ref>:<path>'.
-       * @return {string}
-       */
-      getUrlForRepoDashboard(repo, dashboard) {
-        return this._getUrlFor({
-          view: Gerrit.Nav.View.DASHBOARD,
-          repo,
-          dashboard,
-        });
-      },
+    /**
+     * Navigate to a repo settings page.
+     *
+     * @param {string} repoName
+     */
+    navigateToRepo(repoName) {
+      this._navigate(this.getUrlForRepo(repoName));
+    },
 
-      /**
-       * Navigate to an arbitrary relative URL.
-       *
-       * @param {string} relativeUrl
-       */
-      navigateToRelativeUrl(relativeUrl) {
-        if (!relativeUrl.startsWith('/')) {
-          throw new Error('navigateToRelativeUrl with non-relative URL');
-        }
-        this._navigate(relativeUrl);
-      },
+    /**
+     * @param {string} repoName
+     * @return {string}
+     */
+    getUrlForRepoTags(repoName) {
+      return this._getUrlFor({
+        view: Gerrit.Nav.View.REPO,
+        repoName,
+        detail: Gerrit.Nav.RepoDetailView.TAGS,
+      });
+    },
 
-      /**
-       * @param {string} repoName
-       * @return {string}
-       */
-      getUrlForRepo(repoName) {
-        return this._getUrlFor({
-          view: Gerrit.Nav.View.REPO,
-          repoName,
-        });
-      },
+    /**
+     * @param {string} repoName
+     * @return {string}
+     */
+    getUrlForRepoBranches(repoName) {
+      return this._getUrlFor({
+        view: Gerrit.Nav.View.REPO,
+        repoName,
+        detail: Gerrit.Nav.RepoDetailView.BRANCHES,
+      });
+    },
 
-      /**
-       * Navigate to a repo settings page.
-       *
-       * @param {string} repoName
-       */
-      navigateToRepo(repoName) {
-        this._navigate(this.getUrlForRepo(repoName));
-      },
+    /**
+     * @param {string} repoName
+     * @return {string}
+     */
+    getUrlForRepoAccess(repoName) {
+      return this._getUrlFor({
+        view: Gerrit.Nav.View.REPO,
+        repoName,
+        detail: Gerrit.Nav.RepoDetailView.ACCESS,
+      });
+    },
 
-      /**
-       * @param {string} repoName
-       * @return {string}
-       */
-      getUrlForRepoTags(repoName) {
-        return this._getUrlFor({
-          view: Gerrit.Nav.View.REPO,
-          repoName,
-          detail: Gerrit.Nav.RepoDetailView.TAGS,
-        });
-      },
+    /**
+     * @param {string} repoName
+     * @return {string}
+     */
+    getUrlForRepoCommands(repoName) {
+      return this._getUrlFor({
+        view: Gerrit.Nav.View.REPO,
+        repoName,
+        detail: Gerrit.Nav.RepoDetailView.COMMANDS,
+      });
+    },
 
-      /**
-       * @param {string} repoName
-       * @return {string}
-       */
-      getUrlForRepoBranches(repoName) {
-        return this._getUrlFor({
-          view: Gerrit.Nav.View.REPO,
-          repoName,
-          detail: Gerrit.Nav.RepoDetailView.BRANCHES,
-        });
-      },
+    /**
+     * @param {string} repoName
+     * @return {string}
+     */
+    getUrlForRepoDashboards(repoName) {
+      return this._getUrlFor({
+        view: Gerrit.Nav.View.REPO,
+        repoName,
+        detail: Gerrit.Nav.RepoDetailView.DASHBOARDS,
+      });
+    },
 
-      /**
-       * @param {string} repoName
-       * @return {string}
-       */
-      getUrlForRepoAccess(repoName) {
-        return this._getUrlFor({
-          view: Gerrit.Nav.View.REPO,
-          repoName,
-          detail: Gerrit.Nav.RepoDetailView.ACCESS,
-        });
-      },
+    /**
+     * @param {string} groupId
+     * @return {string}
+     */
+    getUrlForGroup(groupId) {
+      return this._getUrlFor({
+        view: Gerrit.Nav.View.GROUP,
+        groupId,
+      });
+    },
 
-      /**
-       * @param {string} repoName
-       * @return {string}
-       */
-      getUrlForRepoCommands(repoName) {
-        return this._getUrlFor({
-          view: Gerrit.Nav.View.REPO,
-          repoName,
-          detail: Gerrit.Nav.RepoDetailView.COMMANDS,
-        });
-      },
+    /**
+     * @param {string} groupId
+     * @return {string}
+     */
+    getUrlForGroupLog(groupId) {
+      return this._getUrlFor({
+        view: Gerrit.Nav.View.GROUP,
+        groupId,
+        detail: Gerrit.Nav.GroupDetailView.LOG,
+      });
+    },
 
-      /**
-       * @param {string} repoName
-       * @return {string}
-       */
-      getUrlForRepoDashboards(repoName) {
-        return this._getUrlFor({
-          view: Gerrit.Nav.View.REPO,
-          repoName,
-          detail: Gerrit.Nav.RepoDetailView.DASHBOARDS,
-        });
-      },
+    /**
+     * @param {string} groupId
+     * @return {string}
+     */
+    getUrlForGroupMembers(groupId) {
+      return this._getUrlFor({
+        view: Gerrit.Nav.View.GROUP,
+        groupId,
+        detail: Gerrit.Nav.GroupDetailView.MEMBERS,
+      });
+    },
 
-      /**
-       * @param {string} groupId
-       * @return {string}
-       */
-      getUrlForGroup(groupId) {
-        return this._getUrlFor({
-          view: Gerrit.Nav.View.GROUP,
-          groupId,
-        });
-      },
+    getUrlForSettings() {
+      return this._getUrlFor({view: Gerrit.Nav.View.SETTINGS});
+    },
 
-      /**
-       * @param {string} groupId
-       * @return {string}
-       */
-      getUrlForGroupLog(groupId) {
-        return this._getUrlFor({
-          view: Gerrit.Nav.View.GROUP,
-          groupId,
-          detail: Gerrit.Nav.GroupDetailView.LOG,
-        });
-      },
+    /**
+     * @param {string} repo
+     * @param {string} commit
+     * @param {string} file
+     * @param {Object=} opt_options
+     * @return {
+     *   Array<{label: string, url: string}>|
+     *   {label: string, url: string}
+     *  }
+     */
+    getFileWebLinks(repo, commit, file, opt_options) {
+      const params = {type: Gerrit.Nav.WeblinkType.FILE, repo, commit, file};
+      if (opt_options) {
+        params.options = opt_options;
+      }
+      return [].concat(this._generateWeblinks(params));
+    },
 
-      /**
-       * @param {string} groupId
-       * @return {string}
-       */
-      getUrlForGroupMembers(groupId) {
-        return this._getUrlFor({
-          view: Gerrit.Nav.View.GROUP,
-          groupId,
-          detail: Gerrit.Nav.GroupDetailView.MEMBERS,
-        });
-      },
+    /**
+     * @param {string} repo
+     * @param {string} commit
+     * @param {Object=} opt_options
+     * @return {{label: string, url: string}}
+     */
+    getPatchSetWeblink(repo, commit, opt_options) {
+      const params = {type: Gerrit.Nav.WeblinkType.PATCHSET, repo, commit};
+      if (opt_options) {
+        params.options = opt_options;
+      }
+      const result = this._generateWeblinks(params);
+      if (Array.isArray(result)) {
+        return result.pop();
+      } else {
+        return result;
+      }
+    },
 
-      getUrlForSettings() {
-        return this._getUrlFor({view: Gerrit.Nav.View.SETTINGS});
-      },
+    /**
+     * @param {string} repo
+     * @param {string} commit
+     * @param {Object=} opt_options
+     * @return {
+     *   Array<{label: string, url: string}>|
+     *   {label: string, url: string}
+     *  }
+     */
+    getChangeWeblinks(repo, commit, opt_options) {
+      const params = {type: Gerrit.Nav.WeblinkType.CHANGE, repo, commit};
+      if (opt_options) {
+        params.options = opt_options;
+      }
+      return [].concat(this._generateWeblinks(params));
+    },
 
-      /**
-       * @param {string} repo
-       * @param {string} commit
-       * @param {string} file
-       * @param {Object=} opt_options
-       * @return {
-       *   Array<{label: string, url: string}>|
-       *   {label: string, url: string}
-       *  }
-       */
-      getFileWebLinks(repo, commit, file, opt_options) {
-        const params = {type: Gerrit.Nav.WeblinkType.FILE, repo, commit, file};
-        if (opt_options) {
-          params.options = opt_options;
-        }
-        return [].concat(this._generateWeblinks(params));
-      },
-
-      /**
-       * @param {string} repo
-       * @param {string} commit
-       * @param {Object=} opt_options
-       * @return {{label: string, url: string}}
-       */
-      getPatchSetWeblink(repo, commit, opt_options) {
-        const params = {type: Gerrit.Nav.WeblinkType.PATCHSET, repo, commit};
-        if (opt_options) {
-          params.options = opt_options;
-        }
-        const result = this._generateWeblinks(params);
-        if (Array.isArray(result)) {
-          return result.pop();
-        } else {
-          return result;
-        }
-      },
-
-      /**
-       * @param {string} repo
-       * @param {string} commit
-       * @param {Object=} opt_options
-       * @return {
-       *   Array<{label: string, url: string}>|
-       *   {label: string, url: string}
-       *  }
-       */
-      getChangeWeblinks(repo, commit, opt_options) {
-        const params = {type: Gerrit.Nav.WeblinkType.CHANGE, repo, commit};
-        if (opt_options) {
-          params.options = opt_options;
-        }
-        return [].concat(this._generateWeblinks(params));
-      },
-
-      getUserDashboard(user = 'self', sections = DEFAULT_SECTIONS,
-          title = '') {
-        sections = sections
-            .filter(section => (user === 'self' || !section.selfOnly))
-            .map(section => Object.assign({}, section, {
-              name: section.name,
-              query: section.query.replace(USER_PLACEHOLDER_PATTERN, user),
-            }));
-        return {title, sections};
-      },
-    };
-  })(window);
-</script>
+    getUserDashboard(user = 'self', sections = DEFAULT_SECTIONS,
+        title = '') {
+      sections = sections
+          .filter(section => (user === 'self' || !section.selfOnly))
+          .map(section => Object.assign({}, section, {
+            name: section.name,
+            query: section.query.replace(USER_PLACEHOLDER_PATTERN, user),
+          }));
+      return {title, sections};
+    },
+  };
+})(window);
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.html
index f58780c..8f3c623 100644
--- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation_test.html
+++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation_test.html
@@ -19,70 +19,71 @@
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-navigation</title>
 
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
+<script type="module" src="../../../test/test-pre-setup.js"></script>
+<script type="module" src="../../../test/common-test-setup.js"></script>
 
-<script>
-  suite('gr-navigation tests', async () => {
-    await readyToTest();
-    test('invalid patch ranges throw exceptions', () => {
-      assert.throw(() => Gerrit.Nav.getUrlForChange('123', undefined, 12));
-      assert.throw(() => Gerrit.Nav.getUrlForDiff('123', 'x.c', undefined, 12));
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+suite('gr-navigation tests', () => {
+  test('invalid patch ranges throw exceptions', () => {
+    assert.throw(() => Gerrit.Nav.getUrlForChange('123', undefined, 12));
+    assert.throw(() => Gerrit.Nav.getUrlForDiff('123', 'x.c', undefined, 12));
+  });
+
+  suite('_getUserDashboard', () => {
+    const sections = [
+      {name: 'section 1', query: 'query 1'},
+      {name: 'section 2', query: 'query 2 for ${user}'},
+      {name: 'section 3', query: 'self only query', selfOnly: true},
+      {name: 'section 4', query: 'query 4', suffixForDashboard: 'suffix'},
+    ];
+
+    test('dashboard for self', () => {
+      const dashboard =
+          Gerrit.Nav.getUserDashboard('self', sections, 'title');
+      assert.deepEqual(
+          dashboard,
+          {
+            title: 'title',
+            sections: [
+              {name: 'section 1', query: 'query 1'},
+              {name: 'section 2', query: 'query 2 for self'},
+              {
+                name: 'section 3',
+                query: 'self only query',
+                selfOnly: true,
+              }, {
+                name: 'section 4',
+                query: 'query 4',
+                suffixForDashboard: 'suffix',
+              },
+            ],
+          });
     });
 
-    suite('_getUserDashboard', () => {
-      const sections = [
-        {name: 'section 1', query: 'query 1'},
-        {name: 'section 2', query: 'query 2 for ${user}'},
-        {name: 'section 3', query: 'self only query', selfOnly: true},
-        {name: 'section 4', query: 'query 4', suffixForDashboard: 'suffix'},
-      ];
-
-      test('dashboard for self', () => {
-        const dashboard =
-            Gerrit.Nav.getUserDashboard('self', sections, 'title');
-        assert.deepEqual(
-            dashboard,
-            {
-              title: 'title',
-              sections: [
-                {name: 'section 1', query: 'query 1'},
-                {name: 'section 2', query: 'query 2 for self'},
-                {
-                  name: 'section 3',
-                  query: 'self only query',
-                  selfOnly: true,
-                }, {
-                  name: 'section 4',
-                  query: 'query 4',
-                  suffixForDashboard: 'suffix',
-                },
-              ],
-            });
-      });
-
-      test('dashboard for other user', () => {
-        const dashboard =
-            Gerrit.Nav.getUserDashboard('user', sections, 'title');
-        assert.deepEqual(
-            dashboard,
-            {
-              title: 'title',
-              sections: [
-                {name: 'section 1', query: 'query 1'},
-                {name: 'section 2', query: 'query 2 for user'},
-                {
-                  name: 'section 4',
-                  query: 'query 4',
-                  suffixForDashboard: 'suffix',
-                },
-              ],
-            });
-      });
+    test('dashboard for other user', () => {
+      const dashboard =
+          Gerrit.Nav.getUserDashboard('user', sections, 'title');
+      assert.deepEqual(
+          dashboard,
+          {
+            title: 'title',
+            sections: [
+              {name: 'section 1', query: 'query 1'},
+              {name: 'section 2', query: 'query 2 for user'},
+              {
+                name: 'section 4',
+                query: 'query 4',
+                suffixForDashboard: 'suffix',
+              },
+            ],
+          });
     });
   });
+});
 </script>
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.html b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.html
deleted file mode 100644
index 0ba8a22..0000000
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.html
+++ /dev/null
@@ -1,22 +0,0 @@
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-
-<dom-module id="gr-reporting">
-  <script src="gr-reporting.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
index 106508c..55f8abd 100644
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
@@ -14,566 +14,566 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-(function() {
-  'use strict';
+import '../../../scripts/bundled-polymer.js';
 
-  // Latency reporting constants.
-  const TIMING = {
-    TYPE: 'timing-report',
-    CATEGORY_UI_LATENCY: 'UI Latency',
-    CATEGORY_RPC: 'RPC Timing',
-    // Reported events - alphabetize below.
-    APP_STARTED: 'App Started',
-  };
+import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
 
-  // Plugin-related reporting constants.
-  const PLUGINS = {
-    TYPE: 'lifecycle',
-    // Reported events - alphabetize below.
-    INSTALLED: 'Plugins installed',
-  };
+// Latency reporting constants.
+const TIMING = {
+  TYPE: 'timing-report',
+  CATEGORY_UI_LATENCY: 'UI Latency',
+  CATEGORY_RPC: 'RPC Timing',
+  // Reported events - alphabetize below.
+  APP_STARTED: 'App Started',
+};
 
-  // Chrome extension-related reporting constants.
-  const EXTENSION = {
-    TYPE: 'lifecycle',
-    // Reported events - alphabetize below.
-    DETECTED: 'Extension detected',
-  };
+// Plugin-related reporting constants.
+const PLUGINS = {
+  TYPE: 'lifecycle',
+  // Reported events - alphabetize below.
+  INSTALLED: 'Plugins installed',
+};
 
-  // Navigation reporting constants.
-  const NAVIGATION = {
-    TYPE: 'nav-report',
-    CATEGORY: 'Location Changed',
-    PAGE: 'Page',
-  };
+// Chrome extension-related reporting constants.
+const EXTENSION = {
+  TYPE: 'lifecycle',
+  // Reported events - alphabetize below.
+  DETECTED: 'Extension detected',
+};
 
-  const ERROR = {
-    TYPE: 'error',
-    CATEGORY: 'exception',
-  };
+// Navigation reporting constants.
+const NAVIGATION = {
+  TYPE: 'nav-report',
+  CATEGORY: 'Location Changed',
+  PAGE: 'Page',
+};
 
-  const ERROR_DIALOG = {
-    TYPE: 'error',
-    CATEGORY: 'Error Dialog',
-  };
+const ERROR = {
+  TYPE: 'error',
+  CATEGORY: 'exception',
+};
 
-  const TIMER = {
-    CHANGE_DISPLAYED: 'ChangeDisplayed',
-    CHANGE_LOAD_FULL: 'ChangeFullyLoaded',
-    DASHBOARD_DISPLAYED: 'DashboardDisplayed',
-    DIFF_VIEW_CONTENT_DISPLAYED: 'DiffViewOnlyContent',
-    DIFF_VIEW_DISPLAYED: 'DiffViewDisplayed',
-    DIFF_VIEW_LOAD_FULL: 'DiffViewFullyLoaded',
-    FILE_LIST_DISPLAYED: 'FileListDisplayed',
-    PLUGINS_LOADED: 'PluginsLoaded',
-    STARTUP_CHANGE_DISPLAYED: 'StartupChangeDisplayed',
-    STARTUP_CHANGE_LOAD_FULL: 'StartupChangeFullyLoaded',
-    STARTUP_DASHBOARD_DISPLAYED: 'StartupDashboardDisplayed',
-    STARTUP_DIFF_VIEW_CONTENT_DISPLAYED: 'StartupDiffViewOnlyContent',
-    STARTUP_DIFF_VIEW_DISPLAYED: 'StartupDiffViewDisplayed',
-    STARTUP_DIFF_VIEW_LOAD_FULL: 'StartupDiffViewFullyLoaded',
-    STARTUP_FILE_LIST_DISPLAYED: 'StartupFileListDisplayed',
-    WEB_COMPONENTS_READY: 'WebComponentsReady',
-    METRICS_PLUGIN_LOADED: 'MetricsPluginLoaded',
-  };
+const ERROR_DIALOG = {
+  TYPE: 'error',
+  CATEGORY: 'Error Dialog',
+};
 
-  const STARTUP_TIMERS = {};
-  STARTUP_TIMERS[TIMER.PLUGINS_LOADED] = 0;
-  STARTUP_TIMERS[TIMER.METRICS_PLUGIN_LOADED] = 0;
-  STARTUP_TIMERS[TIMER.STARTUP_CHANGE_DISPLAYED] = 0;
-  STARTUP_TIMERS[TIMER.STARTUP_CHANGE_LOAD_FULL] = 0;
-  STARTUP_TIMERS[TIMER.STARTUP_DASHBOARD_DISPLAYED] = 0;
-  STARTUP_TIMERS[TIMER.STARTUP_DIFF_VIEW_CONTENT_DISPLAYED] = 0;
-  STARTUP_TIMERS[TIMER.STARTUP_DIFF_VIEW_DISPLAYED] = 0;
-  STARTUP_TIMERS[TIMER.STARTUP_DIFF_VIEW_LOAD_FULL] = 0;
-  STARTUP_TIMERS[TIMER.STARTUP_FILE_LIST_DISPLAYED] = 0;
-  STARTUP_TIMERS[TIMING.APP_STARTED] = 0;
-  // WebComponentsReady timer is triggered from gr-router.
-  STARTUP_TIMERS[TIMER.WEB_COMPONENTS_READY] = 0;
+const TIMER = {
+  CHANGE_DISPLAYED: 'ChangeDisplayed',
+  CHANGE_LOAD_FULL: 'ChangeFullyLoaded',
+  DASHBOARD_DISPLAYED: 'DashboardDisplayed',
+  DIFF_VIEW_CONTENT_DISPLAYED: 'DiffViewOnlyContent',
+  DIFF_VIEW_DISPLAYED: 'DiffViewDisplayed',
+  DIFF_VIEW_LOAD_FULL: 'DiffViewFullyLoaded',
+  FILE_LIST_DISPLAYED: 'FileListDisplayed',
+  PLUGINS_LOADED: 'PluginsLoaded',
+  STARTUP_CHANGE_DISPLAYED: 'StartupChangeDisplayed',
+  STARTUP_CHANGE_LOAD_FULL: 'StartupChangeFullyLoaded',
+  STARTUP_DASHBOARD_DISPLAYED: 'StartupDashboardDisplayed',
+  STARTUP_DIFF_VIEW_CONTENT_DISPLAYED: 'StartupDiffViewOnlyContent',
+  STARTUP_DIFF_VIEW_DISPLAYED: 'StartupDiffViewDisplayed',
+  STARTUP_DIFF_VIEW_LOAD_FULL: 'StartupDiffViewFullyLoaded',
+  STARTUP_FILE_LIST_DISPLAYED: 'StartupFileListDisplayed',
+  WEB_COMPONENTS_READY: 'WebComponentsReady',
+  METRICS_PLUGIN_LOADED: 'MetricsPluginLoaded',
+};
 
-  const INTERACTION_TYPE = 'interaction';
+const STARTUP_TIMERS = {};
+STARTUP_TIMERS[TIMER.PLUGINS_LOADED] = 0;
+STARTUP_TIMERS[TIMER.METRICS_PLUGIN_LOADED] = 0;
+STARTUP_TIMERS[TIMER.STARTUP_CHANGE_DISPLAYED] = 0;
+STARTUP_TIMERS[TIMER.STARTUP_CHANGE_LOAD_FULL] = 0;
+STARTUP_TIMERS[TIMER.STARTUP_DASHBOARD_DISPLAYED] = 0;
+STARTUP_TIMERS[TIMER.STARTUP_DIFF_VIEW_CONTENT_DISPLAYED] = 0;
+STARTUP_TIMERS[TIMER.STARTUP_DIFF_VIEW_DISPLAYED] = 0;
+STARTUP_TIMERS[TIMER.STARTUP_DIFF_VIEW_LOAD_FULL] = 0;
+STARTUP_TIMERS[TIMER.STARTUP_FILE_LIST_DISPLAYED] = 0;
+STARTUP_TIMERS[TIMING.APP_STARTED] = 0;
+// WebComponentsReady timer is triggered from gr-router.
+STARTUP_TIMERS[TIMER.WEB_COMPONENTS_READY] = 0;
 
-  const DRAFT_ACTION_TIMER = 'TimeBetweenDraftActions';
-  const DRAFT_ACTION_TIMER_MAX = 2 * 60 * 1000; // 2 minutes.
+const INTERACTION_TYPE = 'interaction';
 
-  let pending = [];
-  let slowRpcList = [];
-  const SLOW_RPC_THRESHOLD = 500;
+const DRAFT_ACTION_TIMER = 'TimeBetweenDraftActions';
+const DRAFT_ACTION_TIMER_MAX = 2 * 60 * 1000; // 2 minutes.
 
-  // Variables that hold context info in global scope
-  let reportRepoName = undefined;
+let pending = [];
+let slowRpcList = [];
+const SLOW_RPC_THRESHOLD = 500;
 
-  const onError = function(oldOnError, msg, url, line, column, error) {
-    if (oldOnError) {
-      oldOnError(msg, url, line, column, error);
+// Variables that hold context info in global scope
+let reportRepoName = undefined;
+
+const onError = function(oldOnError, msg, url, line, column, error) {
+  if (oldOnError) {
+    oldOnError(msg, url, line, column, error);
+  }
+  if (error) {
+    line = line || error.lineNumber;
+    column = column || error.columnNumber;
+    let shortenedErrorStack = msg;
+    if (error.stack) {
+      const errorStackLines = error.stack.split('\n');
+      shortenedErrorStack = errorStackLines.slice(0,
+          Math.min(3, errorStackLines.length)).join('\n');
     }
-    if (error) {
-      line = line || error.lineNumber;
-      column = column || error.columnNumber;
-      let shortenedErrorStack = msg;
-      if (error.stack) {
-        const errorStackLines = error.stack.split('\n');
-        shortenedErrorStack = errorStackLines.slice(0,
-            Math.min(3, errorStackLines.length)).join('\n');
-      }
-      msg = shortenedErrorStack || error.toString();
-    }
+    msg = shortenedErrorStack || error.toString();
+  }
+  const payload = {
+    url,
+    line,
+    column,
+    error,
+  };
+  GrReporting.prototype.reporter(ERROR.TYPE, ERROR.CATEGORY, msg, payload);
+  return true;
+};
+
+const catchErrors = function(opt_context) {
+  const context = opt_context || window;
+  context.onerror = onError.bind(null, context.onerror);
+  context.addEventListener('unhandledrejection', e => {
+    const msg = e.reason.message;
     const payload = {
-      url,
-      line,
-      column,
-      error,
+      error: e.reason,
     };
     GrReporting.prototype.reporter(ERROR.TYPE, ERROR.CATEGORY, msg, payload);
-    return true;
-  };
+  });
+};
+catchErrors();
 
-  const catchErrors = function(opt_context) {
-    const context = opt_context || window;
-    context.onerror = onError.bind(null, context.onerror);
-    context.addEventListener('unhandledrejection', e => {
-      const msg = e.reason.message;
-      const payload = {
-        error: e.reason,
-      };
-      GrReporting.prototype.reporter(ERROR.TYPE, ERROR.CATEGORY, msg, payload);
+// PerformanceObserver interface is a browser API.
+if (window.PerformanceObserver) {
+  const supportedEntryTypes = PerformanceObserver.supportedEntryTypes || [];
+  // Safari doesn't support longtask yet
+  if (supportedEntryTypes.includes('longtask')) {
+    const catchLongJsTasks = new PerformanceObserver(list => {
+      for (const task of list.getEntries()) {
+        // We are interested in longtask longer than 200 ms (default is 50 ms)
+        if (task.duration > 200) {
+          GrReporting.prototype.reporter(TIMING.TYPE,
+              TIMING.CATEGORY_UI_LATENCY, `Task ${task.name}`,
+              Math.round(task.duration), {}, false);
+        }
+      }
     });
-  };
-  catchErrors();
-
-  // PerformanceObserver interface is a browser API.
-  if (window.PerformanceObserver) {
-    const supportedEntryTypes = PerformanceObserver.supportedEntryTypes || [];
-    // Safari doesn't support longtask yet
-    if (supportedEntryTypes.includes('longtask')) {
-      const catchLongJsTasks = new PerformanceObserver(list => {
-        for (const task of list.getEntries()) {
-          // We are interested in longtask longer than 200 ms (default is 50 ms)
-          if (task.duration > 200) {
-            GrReporting.prototype.reporter(TIMING.TYPE,
-                TIMING.CATEGORY_UI_LATENCY, `Task ${task.name}`,
-                Math.round(task.duration), {}, false);
-          }
-        }
-      });
-      catchLongJsTasks.observe({entryTypes: ['longtask']});
-    }
+    catchLongJsTasks.observe({entryTypes: ['longtask']});
   }
+}
 
-  document.addEventListener('visibilitychange', () => {
-    const eventName = `Visibility changed to ${document.visibilityState}`;
-    GrReporting.prototype.reporter(INTERACTION_TYPE, undefined, eventName,
-        undefined, {}, true);
-  });
+document.addEventListener('visibilitychange', () => {
+  const eventName = `Visibility changed to ${document.visibilityState}`;
+  GrReporting.prototype.reporter(INTERACTION_TYPE, undefined, eventName,
+      undefined, {}, true);
+});
 
-  // The Polymer pass of JSCompiler requires this to be reassignable
-  // eslint-disable-next-line prefer-const
-  let GrReporting = Polymer({
-    is: 'gr-reporting',
+// The Polymer pass of JSCompiler requires this to be reassignable
+// eslint-disable-next-line prefer-const
+let GrReporting = Polymer({
+  is: 'gr-reporting',
 
-    properties: {
-      category: String,
+  properties: {
+    category: String,
 
-      _baselines: {
-        type: Object,
-        value: STARTUP_TIMERS, // Shared across all instances.
+    _baselines: {
+      type: Object,
+      value: STARTUP_TIMERS, // Shared across all instances.
+    },
+
+    _timers: {
+      type: Object,
+      value: {timeBetweenDraftActions: null}, // Shared across all instances.
+    },
+  },
+
+  get performanceTiming() {
+    return window.performance.timing;
+  },
+
+  get slowRpcSnapshot() {
+    return slowRpcList.slice();
+  },
+
+  now() {
+    return Math.round(window.performance.now());
+  },
+
+  _arePluginsLoaded() {
+    return this._baselines &&
+      !this._baselines.hasOwnProperty(TIMER.PLUGINS_LOADED);
+  },
+
+  _isMetricsPluginLoaded() {
+    return this._arePluginsLoaded() || this._baselines &&
+      !this._baselines.hasOwnProperty(TIMER.METRICS_PLUGIN_LOADED);
+  },
+
+  /**
+   * Reporter reports events. Events will be queued if metrics plugin is not
+   * yet installed.
+   *
+   * @param {string} type
+   * @param {string} category
+   * @param {string} eventName
+   * @param {string|number} eventValue
+   * @param {Object} eventDetails
+   * @param {boolean|undefined} opt_noLog If true, the event will not be
+   *     logged to the JS console.
+   */
+  reporter(type, category, eventName, eventValue, eventDetails, opt_noLog) {
+    const eventInfo = this._createEventInfo(type, category,
+        eventName, eventValue, eventDetails);
+    if (type === ERROR.TYPE && category === ERROR.CATEGORY) {
+      console.error(eventValue && eventValue.error || eventName);
+    }
+
+    // We report events immediately when metrics plugin is loaded
+    if (this._isMetricsPluginLoaded() && !pending.length) {
+      this._reportEvent(eventInfo, opt_noLog);
+    } else {
+      // We cache until metrics plugin is loaded
+      pending.push([eventInfo, opt_noLog]);
+      if (this._isMetricsPluginLoaded()) {
+        pending.forEach(([eventInfo, opt_noLog]) => {
+          this._reportEvent(eventInfo, opt_noLog);
+        });
+        pending = [];
+      }
+    }
+  },
+
+  _reportEvent(eventInfo, opt_noLog) {
+    const {type, value, name} = eventInfo;
+    document.dispatchEvent(new CustomEvent(type, {detail: eventInfo}));
+    if (opt_noLog) { return; }
+    if (type !== ERROR.TYPE) {
+      if (value !== undefined) {
+        console.log(`Reporting: ${name}: ${value}`);
+      } else {
+        console.log(`Reporting: ${name}`);
+      }
+    }
+  },
+
+  _createEventInfo(type, category, name, value, eventDetails) {
+    const eventInfo = {
+      type,
+      category,
+      name,
+      value,
+      eventStart: this.now(),
+    };
+
+    if (typeof(eventDetails) === 'object' &&
+      Object.entries(eventDetails).length !== 0) {
+      eventInfo.eventDetails = JSON.stringify(eventDetails);
+    }
+    if (reportRepoName) {
+      eventInfo.repoName = reportRepoName;
+    }
+    const isInBackgroundTab = document.visibilityState === 'hidden';
+    if (isInBackgroundTab !== undefined) {
+      eventInfo.inBackgroundTab = isInBackgroundTab;
+    }
+
+    return eventInfo;
+  },
+
+  /**
+   * User-perceived app start time, should be reported when the app is ready.
+   */
+  appStarted() {
+    this.timeEnd(TIMING.APP_STARTED);
+    this.pageLoaded();
+  },
+
+  /**
+   * Page load time and other metrics, should be reported at any time
+   * after navigation.
+   */
+  pageLoaded() {
+    if (this.performanceTiming.loadEventEnd === 0) {
+      console.error('pageLoaded should be called after window.onload');
+      this.async(this.pageLoaded, 100);
+    } else {
+      const perfEvents = Object.keys(this.performanceTiming.toJSON());
+      perfEvents.forEach(
+          eventName => this._reportPerformanceTiming(eventName)
+      );
+    }
+  },
+
+  _reportPerformanceTiming(eventName, eventDetails) {
+    const eventTiming = this.performanceTiming[eventName];
+    if (eventTiming > 0) {
+      const elapsedTime = eventTiming -
+          this.performanceTiming.navigationStart;
+      // NavResTime - Navigation and resource timings.
+      this.reporter(TIMING.TYPE, TIMING.CATEGORY_UI_LATENCY,
+          `NavResTime - ${eventName}`, elapsedTime, eventDetails, true);
+    }
+  },
+
+  beforeLocationChanged() {
+    for (const prop of Object.keys(this._baselines)) {
+      delete this._baselines[prop];
+    }
+    this.time(TIMER.CHANGE_DISPLAYED);
+    this.time(TIMER.CHANGE_LOAD_FULL);
+    this.time(TIMER.DASHBOARD_DISPLAYED);
+    this.time(TIMER.DIFF_VIEW_CONTENT_DISPLAYED);
+    this.time(TIMER.DIFF_VIEW_DISPLAYED);
+    this.time(TIMER.DIFF_VIEW_LOAD_FULL);
+    this.time(TIMER.FILE_LIST_DISPLAYED);
+    reportRepoName = undefined;
+    // reset slow rpc list since here start page loads which report these rpcs
+    slowRpcList = [];
+  },
+
+  locationChanged(page) {
+    this.reporter(
+        NAVIGATION.TYPE, NAVIGATION.CATEGORY, NAVIGATION.PAGE, page);
+  },
+
+  dashboardDisplayed() {
+    if (this._baselines.hasOwnProperty(TIMER.STARTUP_DASHBOARD_DISPLAYED)) {
+      this.timeEnd(TIMER.STARTUP_DASHBOARD_DISPLAYED, {rpcList:
+        this.slowRpcSnapshot});
+    } else {
+      this.timeEnd(TIMER.DASHBOARD_DISPLAYED, {rpcList:
+        this.slowRpcSnapshot});
+    }
+  },
+
+  changeDisplayed() {
+    if (this._baselines.hasOwnProperty(TIMER.STARTUP_CHANGE_DISPLAYED)) {
+      this.timeEnd(TIMER.STARTUP_CHANGE_DISPLAYED, {rpcList:
+        this.slowRpcSnapshot});
+    } else {
+      this.timeEnd(TIMER.CHANGE_DISPLAYED, {rpcList:
+        this.slowRpcSnapshot});
+    }
+  },
+
+  changeFullyLoaded() {
+    if (this._baselines.hasOwnProperty(TIMER.STARTUP_CHANGE_LOAD_FULL)) {
+      this.timeEnd(TIMER.STARTUP_CHANGE_LOAD_FULL);
+    } else {
+      this.timeEnd(TIMER.CHANGE_LOAD_FULL);
+    }
+  },
+
+  diffViewDisplayed() {
+    if (this._baselines.hasOwnProperty(TIMER.STARTUP_DIFF_VIEW_DISPLAYED)) {
+      this.timeEnd(TIMER.STARTUP_DIFF_VIEW_DISPLAYED, {rpcList:
+        this.slowRpcSnapshot});
+    } else {
+      this.timeEnd(TIMER.DIFF_VIEW_DISPLAYED, {rpcList:
+        this.slowRpcSnapshot});
+    }
+  },
+
+  diffViewFullyLoaded() {
+    if (this._baselines.hasOwnProperty(TIMER.STARTUP_DIFF_VIEW_LOAD_FULL)) {
+      this.timeEnd(TIMER.STARTUP_DIFF_VIEW_LOAD_FULL);
+    } else {
+      this.timeEnd(TIMER.DIFF_VIEW_LOAD_FULL);
+    }
+  },
+
+  diffViewContentDisplayed() {
+    if (this._baselines.hasOwnProperty(
+        TIMER.STARTUP_DIFF_VIEW_CONTENT_DISPLAYED)) {
+      this.timeEnd(TIMER.STARTUP_DIFF_VIEW_CONTENT_DISPLAYED);
+    } else {
+      this.timeEnd(TIMER.DIFF_VIEW_CONTENT_DISPLAYED);
+    }
+  },
+
+  fileListDisplayed() {
+    if (this._baselines.hasOwnProperty(TIMER.STARTUP_FILE_LIST_DISPLAYED)) {
+      this.timeEnd(TIMER.STARTUP_FILE_LIST_DISPLAYED);
+    } else {
+      this.timeEnd(TIMER.FILE_LIST_DISPLAYED);
+    }
+  },
+
+  reportExtension(name) {
+    this.reporter(EXTENSION.TYPE, EXTENSION.DETECTED, name);
+  },
+
+  pluginLoaded(name) {
+    if (name.startsWith('metrics-')) {
+      this.timeEnd(TIMER.METRICS_PLUGIN_LOADED);
+    }
+  },
+
+  pluginsLoaded(pluginsList) {
+    this.timeEnd(TIMER.PLUGINS_LOADED);
+    this.reporter(
+        PLUGINS.TYPE, PLUGINS.INSTALLED, PLUGINS.INSTALLED, undefined,
+        {pluginsList: pluginsList || []}, true);
+  },
+
+  /**
+   * Reset named timer.
+   */
+  time(name) {
+    this._baselines[name] = this.now();
+    window.performance.mark(`${name}-start`);
+  },
+
+  /**
+   * Finish named timer and report it to server.
+   */
+  timeEnd(name, eventDetails) {
+    if (!this._baselines.hasOwnProperty(name)) { return; }
+    const baseTime = this._baselines[name];
+    delete this._baselines[name];
+    this._reportTiming(name, this.now() - baseTime, eventDetails);
+
+    // Finalize the interval. Either from a registered start mark or
+    // the navigation start time (if baseTime is 0).
+    if (baseTime !== 0) {
+      window.performance.measure(name, `${name}-start`);
+    } else {
+      // Microsft Edge does not handle the 2nd param correctly
+      // (if undefined).
+      window.performance.measure(name);
+    }
+  },
+
+  /**
+   * Reports just line timeEnd, but additionally reports an average given a
+   * denominator and a separate reporiting name for the average.
+   *
+   * @param {string} name Timing name.
+   * @param {string} averageName Average timing name.
+   * @param {number} denominator Number by which to divide the total to
+   *     compute the average.
+   */
+  timeEndWithAverage(name, averageName, denominator) {
+    if (!this._baselines.hasOwnProperty(name)) { return; }
+    const baseTime = this._baselines[name];
+    this.timeEnd(name);
+
+    // Guard against division by zero.
+    if (!denominator) { return; }
+    const time = this.now() - baseTime;
+    this._reportTiming(averageName, time / denominator);
+  },
+
+  /**
+   * Send a timing report with an arbitrary time value.
+   *
+   * @param {string} name Timing name.
+   * @param {number} time The time to report as an integer of milliseconds.
+   * @param {Object} eventDetails non sensitive details
+   */
+  _reportTiming(name, time, eventDetails) {
+    this.reporter(TIMING.TYPE, TIMING.CATEGORY_UI_LATENCY, name, time,
+        eventDetails);
+  },
+
+  /**
+   * Get a timer object to for reporing a user timing. The start time will be
+   * the time that the object has been created, and the end time will be the
+   * time that the "end" method is called on the object.
+   *
+   * @param {string} name Timing name.
+   * @returns {!Object} The timer object.
+   */
+  getTimer(name) {
+    let called = false;
+    let start;
+    let max = null;
+
+    const timer = {
+
+      // Clear the timer and reset the start time.
+      reset: () => {
+        called = false;
+        start = this.now();
+        return timer;
       },
 
-      _timers: {
-        type: Object,
-        value: {timeBetweenDraftActions: null}, // Shared across all instances.
+      // Stop the timer and report the intervening time.
+      end: () => {
+        if (called) {
+          throw new Error(`Timer for "${name}" already ended.`);
+        }
+        called = true;
+        const time = this.now() - start;
+
+        // If a maximum is specified and the time exceeds it, do not report.
+        if (max && time > max) { return timer; }
+
+        this._reportTiming(name, time);
+        return timer;
       },
-    },
 
-    get performanceTiming() {
-      return window.performance.timing;
-    },
+      // Set a maximum reportable time. If a maximum is set and the timer is
+      // ended after the specified amount of time, the value is not reported.
+      withMaximum(maximum) {
+        max = maximum;
+        return timer;
+      },
+    };
 
-    get slowRpcSnapshot() {
-      return slowRpcList.slice();
-    },
+    // The timer is initialized to its creation time.
+    return timer.reset();
+  },
 
-    now() {
-      return Math.round(window.performance.now());
-    },
+  /**
+   * Log timing information for an RPC.
+   *
+   * @param {string} anonymizedUrl The URL of the RPC with tokens obfuscated.
+   * @param {number} elapsed The time elapsed of the RPC.
+   */
+  reportRpcTiming(anonymizedUrl, elapsed) {
+    this.reporter(TIMING.TYPE, TIMING.CATEGORY_RPC, 'RPC-' + anonymizedUrl,
+        elapsed, {}, true);
+    if (elapsed >= SLOW_RPC_THRESHOLD) {
+      slowRpcList.push({anonymizedUrl, elapsed});
+    }
+  },
 
-    _arePluginsLoaded() {
-      return this._baselines &&
-        !this._baselines.hasOwnProperty(TIMER.PLUGINS_LOADED);
-    },
+  reportInteraction(eventName, details) {
+    this.reporter(INTERACTION_TYPE, this.category, eventName, undefined,
+        details, true);
+  },
 
-    _isMetricsPluginLoaded() {
-      return this._arePluginsLoaded() || this._baselines &&
-        !this._baselines.hasOwnProperty(TIMER.METRICS_PLUGIN_LOADED);
-    },
+  /**
+   * A draft interaction was started. Update the time-betweeen-draft-actions
+   * timer.
+   */
+  recordDraftInteraction() {
+    // If there is no timer defined, then this is the first interaction.
+    // Set up the timer so that it's ready to record the intervening time when
+    // called again.
+    const timer = this._timers.timeBetweenDraftActions;
+    if (!timer) {
+      // Create a timer with a maximum length.
+      this._timers.timeBetweenDraftActions = this.getTimer(DRAFT_ACTION_TIMER)
+          .withMaximum(DRAFT_ACTION_TIMER_MAX);
+      return;
+    }
 
-    /**
-     * Reporter reports events. Events will be queued if metrics plugin is not
-     * yet installed.
-     *
-     * @param {string} type
-     * @param {string} category
-     * @param {string} eventName
-     * @param {string|number} eventValue
-     * @param {Object} eventDetails
-     * @param {boolean|undefined} opt_noLog If true, the event will not be
-     *     logged to the JS console.
-     */
-    reporter(type, category, eventName, eventValue, eventDetails, opt_noLog) {
-      const eventInfo = this._createEventInfo(type, category,
-          eventName, eventValue, eventDetails);
-      if (type === ERROR.TYPE && category === ERROR.CATEGORY) {
-        console.error(eventValue && eventValue.error || eventName);
-      }
+    // Mark the time and reinitialize the timer.
+    timer.end().reset();
+  },
 
-      // We report events immediately when metrics plugin is loaded
-      if (this._isMetricsPluginLoaded() && !pending.length) {
-        this._reportEvent(eventInfo, opt_noLog);
-      } else {
-        // We cache until metrics plugin is loaded
-        pending.push([eventInfo, opt_noLog]);
-        if (this._isMetricsPluginLoaded()) {
-          pending.forEach(([eventInfo, opt_noLog]) => {
-            this._reportEvent(eventInfo, opt_noLog);
-          });
-          pending = [];
-        }
-      }
-    },
+  reportErrorDialog(message) {
+    this.reporter(ERROR_DIALOG.TYPE, ERROR_DIALOG.CATEGORY,
+        'ErrorDialog: ' + message, {error: new Error(message)});
+  },
 
-    _reportEvent(eventInfo, opt_noLog) {
-      const {type, value, name} = eventInfo;
-      document.dispatchEvent(new CustomEvent(type, {detail: eventInfo}));
-      if (opt_noLog) { return; }
-      if (type !== ERROR.TYPE) {
-        if (value !== undefined) {
-          console.log(`Reporting: ${name}: ${value}`);
-        } else {
-          console.log(`Reporting: ${name}`);
-        }
-      }
-    },
+  setRepoName(repoName) {
+    reportRepoName = repoName;
+  },
+});
 
-    _createEventInfo(type, category, name, value, eventDetails) {
-      const eventInfo = {
-        type,
-        category,
-        name,
-        value,
-        eventStart: this.now(),
-      };
-
-      if (typeof(eventDetails) === 'object' &&
-        Object.entries(eventDetails).length !== 0) {
-        eventInfo.eventDetails = JSON.stringify(eventDetails);
-      }
-      if (reportRepoName) {
-        eventInfo.repoName = reportRepoName;
-      }
-      const isInBackgroundTab = document.visibilityState === 'hidden';
-      if (isInBackgroundTab !== undefined) {
-        eventInfo.inBackgroundTab = isInBackgroundTab;
-      }
-
-      return eventInfo;
-    },
-
-    /**
-     * User-perceived app start time, should be reported when the app is ready.
-     */
-    appStarted() {
-      this.timeEnd(TIMING.APP_STARTED);
-      this.pageLoaded();
-    },
-
-    /**
-     * Page load time and other metrics, should be reported at any time
-     * after navigation.
-     */
-    pageLoaded() {
-      if (this.performanceTiming.loadEventEnd === 0) {
-        console.error('pageLoaded should be called after window.onload');
-        this.async(this.pageLoaded, 100);
-      } else {
-        const perfEvents = Object.keys(this.performanceTiming.toJSON());
-        perfEvents.forEach(
-            eventName => this._reportPerformanceTiming(eventName)
-        );
-      }
-    },
-
-    _reportPerformanceTiming(eventName, eventDetails) {
-      const eventTiming = this.performanceTiming[eventName];
-      if (eventTiming > 0) {
-        const elapsedTime = eventTiming -
-            this.performanceTiming.navigationStart;
-        // NavResTime - Navigation and resource timings.
-        this.reporter(TIMING.TYPE, TIMING.CATEGORY_UI_LATENCY,
-            `NavResTime - ${eventName}`, elapsedTime, eventDetails, true);
-      }
-    },
-
-    beforeLocationChanged() {
-      for (const prop of Object.keys(this._baselines)) {
-        delete this._baselines[prop];
-      }
-      this.time(TIMER.CHANGE_DISPLAYED);
-      this.time(TIMER.CHANGE_LOAD_FULL);
-      this.time(TIMER.DASHBOARD_DISPLAYED);
-      this.time(TIMER.DIFF_VIEW_CONTENT_DISPLAYED);
-      this.time(TIMER.DIFF_VIEW_DISPLAYED);
-      this.time(TIMER.DIFF_VIEW_LOAD_FULL);
-      this.time(TIMER.FILE_LIST_DISPLAYED);
-      reportRepoName = undefined;
-      // reset slow rpc list since here start page loads which report these rpcs
-      slowRpcList = [];
-    },
-
-    locationChanged(page) {
-      this.reporter(
-          NAVIGATION.TYPE, NAVIGATION.CATEGORY, NAVIGATION.PAGE, page);
-    },
-
-    dashboardDisplayed() {
-      if (this._baselines.hasOwnProperty(TIMER.STARTUP_DASHBOARD_DISPLAYED)) {
-        this.timeEnd(TIMER.STARTUP_DASHBOARD_DISPLAYED, {rpcList:
-          this.slowRpcSnapshot});
-      } else {
-        this.timeEnd(TIMER.DASHBOARD_DISPLAYED, {rpcList:
-          this.slowRpcSnapshot});
-      }
-    },
-
-    changeDisplayed() {
-      if (this._baselines.hasOwnProperty(TIMER.STARTUP_CHANGE_DISPLAYED)) {
-        this.timeEnd(TIMER.STARTUP_CHANGE_DISPLAYED, {rpcList:
-          this.slowRpcSnapshot});
-      } else {
-        this.timeEnd(TIMER.CHANGE_DISPLAYED, {rpcList:
-          this.slowRpcSnapshot});
-      }
-    },
-
-    changeFullyLoaded() {
-      if (this._baselines.hasOwnProperty(TIMER.STARTUP_CHANGE_LOAD_FULL)) {
-        this.timeEnd(TIMER.STARTUP_CHANGE_LOAD_FULL);
-      } else {
-        this.timeEnd(TIMER.CHANGE_LOAD_FULL);
-      }
-    },
-
-    diffViewDisplayed() {
-      if (this._baselines.hasOwnProperty(TIMER.STARTUP_DIFF_VIEW_DISPLAYED)) {
-        this.timeEnd(TIMER.STARTUP_DIFF_VIEW_DISPLAYED, {rpcList:
-          this.slowRpcSnapshot});
-      } else {
-        this.timeEnd(TIMER.DIFF_VIEW_DISPLAYED, {rpcList:
-          this.slowRpcSnapshot});
-      }
-    },
-
-    diffViewFullyLoaded() {
-      if (this._baselines.hasOwnProperty(TIMER.STARTUP_DIFF_VIEW_LOAD_FULL)) {
-        this.timeEnd(TIMER.STARTUP_DIFF_VIEW_LOAD_FULL);
-      } else {
-        this.timeEnd(TIMER.DIFF_VIEW_LOAD_FULL);
-      }
-    },
-
-    diffViewContentDisplayed() {
-      if (this._baselines.hasOwnProperty(
-          TIMER.STARTUP_DIFF_VIEW_CONTENT_DISPLAYED)) {
-        this.timeEnd(TIMER.STARTUP_DIFF_VIEW_CONTENT_DISPLAYED);
-      } else {
-        this.timeEnd(TIMER.DIFF_VIEW_CONTENT_DISPLAYED);
-      }
-    },
-
-    fileListDisplayed() {
-      if (this._baselines.hasOwnProperty(TIMER.STARTUP_FILE_LIST_DISPLAYED)) {
-        this.timeEnd(TIMER.STARTUP_FILE_LIST_DISPLAYED);
-      } else {
-        this.timeEnd(TIMER.FILE_LIST_DISPLAYED);
-      }
-    },
-
-    reportExtension(name) {
-      this.reporter(EXTENSION.TYPE, EXTENSION.DETECTED, name);
-    },
-
-    pluginLoaded(name) {
-      if (name.startsWith('metrics-')) {
-        this.timeEnd(TIMER.METRICS_PLUGIN_LOADED);
-      }
-    },
-
-    pluginsLoaded(pluginsList) {
-      this.timeEnd(TIMER.PLUGINS_LOADED);
-      this.reporter(
-          PLUGINS.TYPE, PLUGINS.INSTALLED, PLUGINS.INSTALLED, undefined,
-          {pluginsList: pluginsList || []}, true);
-    },
-
-    /**
-     * Reset named timer.
-     */
-    time(name) {
-      this._baselines[name] = this.now();
-      window.performance.mark(`${name}-start`);
-    },
-
-    /**
-     * Finish named timer and report it to server.
-     */
-    timeEnd(name, eventDetails) {
-      if (!this._baselines.hasOwnProperty(name)) { return; }
-      const baseTime = this._baselines[name];
-      delete this._baselines[name];
-      this._reportTiming(name, this.now() - baseTime, eventDetails);
-
-      // Finalize the interval. Either from a registered start mark or
-      // the navigation start time (if baseTime is 0).
-      if (baseTime !== 0) {
-        window.performance.measure(name, `${name}-start`);
-      } else {
-        // Microsft Edge does not handle the 2nd param correctly
-        // (if undefined).
-        window.performance.measure(name);
-      }
-    },
-
-    /**
-     * Reports just line timeEnd, but additionally reports an average given a
-     * denominator and a separate reporiting name for the average.
-     *
-     * @param {string} name Timing name.
-     * @param {string} averageName Average timing name.
-     * @param {number} denominator Number by which to divide the total to
-     *     compute the average.
-     */
-    timeEndWithAverage(name, averageName, denominator) {
-      if (!this._baselines.hasOwnProperty(name)) { return; }
-      const baseTime = this._baselines[name];
-      this.timeEnd(name);
-
-      // Guard against division by zero.
-      if (!denominator) { return; }
-      const time = this.now() - baseTime;
-      this._reportTiming(averageName, time / denominator);
-    },
-
-    /**
-     * Send a timing report with an arbitrary time value.
-     *
-     * @param {string} name Timing name.
-     * @param {number} time The time to report as an integer of milliseconds.
-     * @param {Object} eventDetails non sensitive details
-     */
-    _reportTiming(name, time, eventDetails) {
-      this.reporter(TIMING.TYPE, TIMING.CATEGORY_UI_LATENCY, name, time,
-          eventDetails);
-    },
-
-    /**
-     * Get a timer object to for reporing a user timing. The start time will be
-     * the time that the object has been created, and the end time will be the
-     * time that the "end" method is called on the object.
-     *
-     * @param {string} name Timing name.
-     * @returns {!Object} The timer object.
-     */
-    getTimer(name) {
-      let called = false;
-      let start;
-      let max = null;
-
-      const timer = {
-
-        // Clear the timer and reset the start time.
-        reset: () => {
-          called = false;
-          start = this.now();
-          return timer;
-        },
-
-        // Stop the timer and report the intervening time.
-        end: () => {
-          if (called) {
-            throw new Error(`Timer for "${name}" already ended.`);
-          }
-          called = true;
-          const time = this.now() - start;
-
-          // If a maximum is specified and the time exceeds it, do not report.
-          if (max && time > max) { return timer; }
-
-          this._reportTiming(name, time);
-          return timer;
-        },
-
-        // Set a maximum reportable time. If a maximum is set and the timer is
-        // ended after the specified amount of time, the value is not reported.
-        withMaximum(maximum) {
-          max = maximum;
-          return timer;
-        },
-      };
-
-      // The timer is initialized to its creation time.
-      return timer.reset();
-    },
-
-    /**
-     * Log timing information for an RPC.
-     *
-     * @param {string} anonymizedUrl The URL of the RPC with tokens obfuscated.
-     * @param {number} elapsed The time elapsed of the RPC.
-     */
-    reportRpcTiming(anonymizedUrl, elapsed) {
-      this.reporter(TIMING.TYPE, TIMING.CATEGORY_RPC, 'RPC-' + anonymizedUrl,
-          elapsed, {}, true);
-      if (elapsed >= SLOW_RPC_THRESHOLD) {
-        slowRpcList.push({anonymizedUrl, elapsed});
-      }
-    },
-
-    reportInteraction(eventName, details) {
-      this.reporter(INTERACTION_TYPE, this.category, eventName, undefined,
-          details, true);
-    },
-
-    /**
-     * A draft interaction was started. Update the time-betweeen-draft-actions
-     * timer.
-     */
-    recordDraftInteraction() {
-      // If there is no timer defined, then this is the first interaction.
-      // Set up the timer so that it's ready to record the intervening time when
-      // called again.
-      const timer = this._timers.timeBetweenDraftActions;
-      if (!timer) {
-        // Create a timer with a maximum length.
-        this._timers.timeBetweenDraftActions = this.getTimer(DRAFT_ACTION_TIMER)
-            .withMaximum(DRAFT_ACTION_TIMER_MAX);
-        return;
-      }
-
-      // Mark the time and reinitialize the timer.
-      timer.end().reset();
-    },
-
-    reportErrorDialog(message) {
-      this.reporter(ERROR_DIALOG.TYPE, ERROR_DIALOG.CATEGORY,
-          'ErrorDialog: ' + message, {error: new Error(message)});
-    },
-
-    setRepoName(repoName) {
-      reportRepoName = repoName;
-    },
-  });
-
-  window.GrReporting = GrReporting;
-  // Expose onerror installation so it would be accessible from tests.
-  window.GrReporting._catchErrors = catchErrors;
-  window.GrReporting.STARTUP_TIMERS = Object.assign({}, STARTUP_TIMERS);
-})();
+window.GrReporting = GrReporting;
+// Expose onerror installation so it would be accessible from tests.
+window.GrReporting._catchErrors = catchErrors;
+window.GrReporting.STARTUP_TIMERS = Object.assign({}, STARTUP_TIMERS);
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
index 19e4b74..ad9903e 100644
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
@@ -19,15 +19,20 @@
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-reporting</title>
 
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-reporting.html">
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
+<script type="module" src="../../../test/test-pre-setup.js"></script>
+<script type="module" src="../../../test/common-test-setup.js"></script>
+<script type="module" src="./gr-reporting.js"></script>
 
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-reporting.js';
+void(0);
+</script>
 
 <test-fixture id="basic">
   <template>
@@ -35,407 +40,409 @@
   </template>
 </test-fixture>
 
-<script>
-  suite('gr-reporting tests', async () => {
-    await readyToTest();
-    let element;
-    let sandbox;
-    let clock;
-    let fakePerformance;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-reporting.js';
+suite('gr-reporting tests', () => {
+  let element;
+  let sandbox;
+  let clock;
+  let fakePerformance;
 
-    const NOW_TIME = 100;
+  const NOW_TIME = 100;
 
+  setup(() => {
+    sandbox = sinon.sandbox.create();
+    clock = sinon.useFakeTimers(NOW_TIME);
+    element = fixture('basic');
+    element._baselines = Object.assign({}, GrReporting.STARTUP_TIMERS);
+    fakePerformance = {
+      navigationStart: 1,
+      loadEventEnd: 2,
+    };
+    fakePerformance.toJSON = () => fakePerformance;
+    sinon.stub(element, 'performanceTiming',
+        {get() { return fakePerformance; }});
+    sandbox.stub(element, 'reporter');
+  });
+
+  teardown(() => {
+    sandbox.restore();
+    clock.restore();
+  });
+
+  test('appStarted', () => {
+    sandbox.stub(element, 'now').returns(42);
+    element.appStarted();
+    assert.isTrue(
+        element.reporter.calledWithMatch(
+            'timing-report', 'UI Latency', 'App Started', 42
+        ));
+  });
+
+  test('WebComponentsReady', () => {
+    sandbox.stub(element, 'now').returns(42);
+    element.timeEnd('WebComponentsReady');
+    assert.isTrue(element.reporter.calledWithMatch(
+        'timing-report', 'UI Latency', 'WebComponentsReady', 42
+    ));
+  });
+
+  test('pageLoaded', () => {
+    element.pageLoaded();
+    assert.isTrue(
+        element.reporter.calledWithExactly(
+            'timing-report', 'UI Latency', 'NavResTime - loadEventEnd',
+            fakePerformance.loadEventEnd - fakePerformance.navigationStart,
+            undefined, true)
+    );
+  });
+
+  test('beforeLocationChanged', () => {
+    element._baselines['garbage'] = 'monster';
+    sandbox.stub(element, 'time');
+    element.beforeLocationChanged();
+    assert.isTrue(element.time.calledWithExactly('DashboardDisplayed'));
+    assert.isTrue(element.time.calledWithExactly('ChangeDisplayed'));
+    assert.isTrue(element.time.calledWithExactly('ChangeFullyLoaded'));
+    assert.isTrue(element.time.calledWithExactly('DiffViewDisplayed'));
+    assert.isTrue(element.time.calledWithExactly('FileListDisplayed'));
+    assert.isFalse(element._baselines.hasOwnProperty('garbage'));
+  });
+
+  test('changeDisplayed', () => {
+    sandbox.spy(element, 'timeEnd');
+    element.changeDisplayed();
+    assert.isFalse(
+        element.timeEnd.calledWithExactly('ChangeDisplayed', {rpcList: []}));
+    assert.isTrue(
+        element.timeEnd.calledWithExactly('StartupChangeDisplayed',
+            {rpcList: []}));
+    element.changeDisplayed();
+    assert.isTrue(element.timeEnd.calledWithExactly('ChangeDisplayed',
+        {rpcList: []}));
+  });
+
+  test('changeFullyLoaded', () => {
+    sandbox.spy(element, 'timeEnd');
+    element.changeFullyLoaded();
+    assert.isFalse(
+        element.timeEnd.calledWithExactly('ChangeFullyLoaded'));
+    assert.isTrue(
+        element.timeEnd.calledWithExactly('StartupChangeFullyLoaded'));
+    element.changeFullyLoaded();
+    assert.isTrue(element.timeEnd.calledWithExactly('ChangeFullyLoaded'));
+  });
+
+  test('diffViewDisplayed', () => {
+    sandbox.spy(element, 'timeEnd');
+    element.diffViewDisplayed();
+    assert.isFalse(
+        element.timeEnd.calledWithExactly('DiffViewDisplayed',
+            {rpcList: []}));
+    assert.isTrue(
+        element.timeEnd.calledWithExactly('StartupDiffViewDisplayed',
+            {rpcList: []}));
+    element.diffViewDisplayed();
+    assert.isTrue(element.timeEnd.calledWithExactly('DiffViewDisplayed',
+        {rpcList: []}));
+  });
+
+  test('fileListDisplayed', () => {
+    sandbox.spy(element, 'timeEnd');
+    element.fileListDisplayed();
+    assert.isFalse(
+        element.timeEnd.calledWithExactly('FileListDisplayed'));
+    assert.isTrue(
+        element.timeEnd.calledWithExactly('StartupFileListDisplayed'));
+    element.fileListDisplayed();
+    assert.isTrue(element.timeEnd.calledWithExactly('FileListDisplayed'));
+  });
+
+  test('dashboardDisplayed', () => {
+    sandbox.spy(element, 'timeEnd');
+    element.dashboardDisplayed();
+    assert.isFalse(
+        element.timeEnd.calledWithExactly('DashboardDisplayed',
+            {rpcList: []}));
+    assert.isTrue(
+        element.timeEnd.calledWithExactly('StartupDashboardDisplayed',
+            {rpcList: []}));
+    element.dashboardDisplayed();
+    assert.isTrue(element.timeEnd.calledWithExactly('DashboardDisplayed',
+        {rpcList: []}));
+  });
+
+  test('dashboardDisplayed', () => {
+    sandbox.spy(element, 'timeEnd');
+    element.reportRpcTiming('/changes/*~*/comments', 500);
+    element.dashboardDisplayed();
+    assert.isTrue(
+        element.timeEnd.calledWithExactly('StartupDashboardDisplayed',
+            {rpcList: [
+              {
+                anonymizedUrl: '/changes/*~*/comments',
+                elapsed: 500,
+              },
+            ]}
+        ));
+  });
+
+  test('time and timeEnd', () => {
+    const nowStub = sandbox.stub(element, 'now').returns(0);
+    element.time('foo');
+    nowStub.returns(1);
+    element.time('bar');
+    nowStub.returns(2);
+    element.timeEnd('bar');
+    nowStub.returns(3);
+    element.timeEnd('foo');
+    assert.isTrue(element.reporter.calledWithMatch(
+        'timing-report', 'UI Latency', 'foo', 3
+    ));
+    assert.isTrue(element.reporter.calledWithMatch(
+        'timing-report', 'UI Latency', 'bar', 1
+    ));
+  });
+
+  test('timer object', () => {
+    const nowStub = sandbox.stub(element, 'now').returns(100);
+    const timer = element.getTimer('foo-bar');
+    nowStub.returns(150);
+    timer.end();
+    assert.isTrue(element.reporter.calledWithMatch(
+        'timing-report', 'UI Latency', 'foo-bar', 50));
+  });
+
+  test('timer object double call', () => {
+    const timer = element.getTimer('foo-bar');
+    timer.end();
+    assert.isTrue(element.reporter.calledOnce);
+    assert.throws(() => {
+      timer.end();
+    }, 'Timer for "foo-bar" already ended.');
+  });
+
+  test('timer object maximum', () => {
+    const nowStub = sandbox.stub(element, 'now').returns(100);
+    const timer = element.getTimer('foo-bar').withMaximum(100);
+    nowStub.returns(150);
+    timer.end();
+    assert.isTrue(element.reporter.calledOnce);
+
+    timer.reset();
+    nowStub.returns(260);
+    timer.end();
+    assert.isTrue(element.reporter.calledOnce);
+  });
+
+  test('recordDraftInteraction', () => {
+    const key = 'TimeBetweenDraftActions';
+    const nowStub = sandbox.stub(element, 'now').returns(100);
+    const timingStub = sandbox.stub(element, '_reportTiming');
+    element.recordDraftInteraction();
+    assert.isFalse(timingStub.called);
+
+    nowStub.returns(200);
+    element.recordDraftInteraction();
+    assert.isTrue(timingStub.calledOnce);
+    assert.equal(timingStub.lastCall.args[0], key);
+    assert.equal(timingStub.lastCall.args[1], 100);
+
+    nowStub.returns(350);
+    element.recordDraftInteraction();
+    assert.isTrue(timingStub.calledTwice);
+    assert.equal(timingStub.lastCall.args[0], key);
+    assert.equal(timingStub.lastCall.args[1], 150);
+
+    nowStub.returns(370 + 2 * 60 * 1000);
+    element.recordDraftInteraction();
+    assert.isFalse(timingStub.calledThrice);
+  });
+
+  test('timeEndWithAverage', () => {
+    const nowStub = sandbox.stub(element, 'now').returns(0);
+    nowStub.returns(1000);
+    element.time('foo');
+    nowStub.returns(1100);
+    element.timeEndWithAverage('foo', 'bar', 10);
+    assert.isTrue(element.reporter.calledTwice);
+    assert.isTrue(element.reporter.calledWithMatch(
+        'timing-report', 'UI Latency', 'foo', 100));
+    assert.isTrue(element.reporter.calledWithMatch(
+        'timing-report', 'UI Latency', 'bar', 10));
+  });
+
+  test('reportExtension', () => {
+    element.reportExtension('foo');
+    assert.isTrue(element.reporter.calledWithExactly(
+        'lifecycle', 'Extension detected', 'foo'
+    ));
+  });
+
+  test('reportInteraction', () => {
+    element.reporter.restore();
+    sandbox.spy(element, '_reportEvent');
+    element.pluginsLoaded(); // so we don't cache
+    element.reportInteraction('button-click', {name: 'sendReply'});
+    assert.isTrue(element._reportEvent.getCall(2).calledWithMatch(
+        {
+          type: 'interaction',
+          name: 'button-click',
+          eventDetails: JSON.stringify({name: 'sendReply'}),
+        }
+    ));
+  });
+
+  test('report start time', () => {
+    element.reporter.restore();
+    sandbox.stub(element, 'now').returns(42);
+    sandbox.spy(element, '_reportEvent');
+    const dispatchStub = sandbox.spy(document, 'dispatchEvent');
+    element.pluginsLoaded();
+    element.time('timeAction');
+    element.timeEnd('timeAction');
+    assert.isTrue(element._reportEvent.getCall(2).calledWithMatch(
+        {
+          type: 'timing-report',
+          category: 'UI Latency',
+          name: 'timeAction',
+          value: 0,
+          eventStart: 42,
+        }
+    ));
+    assert.equal(dispatchStub.getCall(2).args[0].detail.eventStart, 42);
+  });
+
+  suite('plugins', () => {
     setup(() => {
-      sandbox = sinon.sandbox.create();
-      clock = sinon.useFakeTimers(NOW_TIME);
-      element = fixture('basic');
-      element._baselines = Object.assign({}, GrReporting.STARTUP_TIMERS);
-      fakePerformance = {
-        navigationStart: 1,
-        loadEventEnd: 2,
-      };
-      fakePerformance.toJSON = () => fakePerformance;
-      sinon.stub(element, 'performanceTiming',
-          {get() { return fakePerformance; }});
-      sandbox.stub(element, 'reporter');
-    });
-
-    teardown(() => {
-      sandbox.restore();
-      clock.restore();
-    });
-
-    test('appStarted', () => {
-      sandbox.stub(element, 'now').returns(42);
-      element.appStarted();
-      assert.isTrue(
-          element.reporter.calledWithMatch(
-              'timing-report', 'UI Latency', 'App Started', 42
-          ));
-    });
-
-    test('WebComponentsReady', () => {
-      sandbox.stub(element, 'now').returns(42);
-      element.timeEnd('WebComponentsReady');
-      assert.isTrue(element.reporter.calledWithMatch(
-          'timing-report', 'UI Latency', 'WebComponentsReady', 42
-      ));
-    });
-
-    test('pageLoaded', () => {
-      element.pageLoaded();
-      assert.isTrue(
-          element.reporter.calledWithExactly(
-              'timing-report', 'UI Latency', 'NavResTime - loadEventEnd',
-              fakePerformance.loadEventEnd - fakePerformance.navigationStart,
-              undefined, true)
-      );
-    });
-
-    test('beforeLocationChanged', () => {
-      element._baselines['garbage'] = 'monster';
-      sandbox.stub(element, 'time');
-      element.beforeLocationChanged();
-      assert.isTrue(element.time.calledWithExactly('DashboardDisplayed'));
-      assert.isTrue(element.time.calledWithExactly('ChangeDisplayed'));
-      assert.isTrue(element.time.calledWithExactly('ChangeFullyLoaded'));
-      assert.isTrue(element.time.calledWithExactly('DiffViewDisplayed'));
-      assert.isTrue(element.time.calledWithExactly('FileListDisplayed'));
-      assert.isFalse(element._baselines.hasOwnProperty('garbage'));
-    });
-
-    test('changeDisplayed', () => {
-      sandbox.spy(element, 'timeEnd');
-      element.changeDisplayed();
-      assert.isFalse(
-          element.timeEnd.calledWithExactly('ChangeDisplayed', {rpcList: []}));
-      assert.isTrue(
-          element.timeEnd.calledWithExactly('StartupChangeDisplayed',
-              {rpcList: []}));
-      element.changeDisplayed();
-      assert.isTrue(element.timeEnd.calledWithExactly('ChangeDisplayed',
-          {rpcList: []}));
-    });
-
-    test('changeFullyLoaded', () => {
-      sandbox.spy(element, 'timeEnd');
-      element.changeFullyLoaded();
-      assert.isFalse(
-          element.timeEnd.calledWithExactly('ChangeFullyLoaded'));
-      assert.isTrue(
-          element.timeEnd.calledWithExactly('StartupChangeFullyLoaded'));
-      element.changeFullyLoaded();
-      assert.isTrue(element.timeEnd.calledWithExactly('ChangeFullyLoaded'));
-    });
-
-    test('diffViewDisplayed', () => {
-      sandbox.spy(element, 'timeEnd');
-      element.diffViewDisplayed();
-      assert.isFalse(
-          element.timeEnd.calledWithExactly('DiffViewDisplayed',
-              {rpcList: []}));
-      assert.isTrue(
-          element.timeEnd.calledWithExactly('StartupDiffViewDisplayed',
-              {rpcList: []}));
-      element.diffViewDisplayed();
-      assert.isTrue(element.timeEnd.calledWithExactly('DiffViewDisplayed',
-          {rpcList: []}));
-    });
-
-    test('fileListDisplayed', () => {
-      sandbox.spy(element, 'timeEnd');
-      element.fileListDisplayed();
-      assert.isFalse(
-          element.timeEnd.calledWithExactly('FileListDisplayed'));
-      assert.isTrue(
-          element.timeEnd.calledWithExactly('StartupFileListDisplayed'));
-      element.fileListDisplayed();
-      assert.isTrue(element.timeEnd.calledWithExactly('FileListDisplayed'));
-    });
-
-    test('dashboardDisplayed', () => {
-      sandbox.spy(element, 'timeEnd');
-      element.dashboardDisplayed();
-      assert.isFalse(
-          element.timeEnd.calledWithExactly('DashboardDisplayed',
-              {rpcList: []}));
-      assert.isTrue(
-          element.timeEnd.calledWithExactly('StartupDashboardDisplayed',
-              {rpcList: []}));
-      element.dashboardDisplayed();
-      assert.isTrue(element.timeEnd.calledWithExactly('DashboardDisplayed',
-          {rpcList: []}));
-    });
-
-    test('dashboardDisplayed', () => {
-      sandbox.spy(element, 'timeEnd');
-      element.reportRpcTiming('/changes/*~*/comments', 500);
-      element.dashboardDisplayed();
-      assert.isTrue(
-          element.timeEnd.calledWithExactly('StartupDashboardDisplayed',
-              {rpcList: [
-                {
-                  anonymizedUrl: '/changes/*~*/comments',
-                  elapsed: 500,
-                },
-              ]}
-          ));
-    });
-
-    test('time and timeEnd', () => {
-      const nowStub = sandbox.stub(element, 'now').returns(0);
-      element.time('foo');
-      nowStub.returns(1);
-      element.time('bar');
-      nowStub.returns(2);
-      element.timeEnd('bar');
-      nowStub.returns(3);
-      element.timeEnd('foo');
-      assert.isTrue(element.reporter.calledWithMatch(
-          'timing-report', 'UI Latency', 'foo', 3
-      ));
-      assert.isTrue(element.reporter.calledWithMatch(
-          'timing-report', 'UI Latency', 'bar', 1
-      ));
-    });
-
-    test('timer object', () => {
-      const nowStub = sandbox.stub(element, 'now').returns(100);
-      const timer = element.getTimer('foo-bar');
-      nowStub.returns(150);
-      timer.end();
-      assert.isTrue(element.reporter.calledWithMatch(
-          'timing-report', 'UI Latency', 'foo-bar', 50));
-    });
-
-    test('timer object double call', () => {
-      const timer = element.getTimer('foo-bar');
-      timer.end();
-      assert.isTrue(element.reporter.calledOnce);
-      assert.throws(() => {
-        timer.end();
-      }, 'Timer for "foo-bar" already ended.');
-    });
-
-    test('timer object maximum', () => {
-      const nowStub = sandbox.stub(element, 'now').returns(100);
-      const timer = element.getTimer('foo-bar').withMaximum(100);
-      nowStub.returns(150);
-      timer.end();
-      assert.isTrue(element.reporter.calledOnce);
-
-      timer.reset();
-      nowStub.returns(260);
-      timer.end();
-      assert.isTrue(element.reporter.calledOnce);
-    });
-
-    test('recordDraftInteraction', () => {
-      const key = 'TimeBetweenDraftActions';
-      const nowStub = sandbox.stub(element, 'now').returns(100);
-      const timingStub = sandbox.stub(element, '_reportTiming');
-      element.recordDraftInteraction();
-      assert.isFalse(timingStub.called);
-
-      nowStub.returns(200);
-      element.recordDraftInteraction();
-      assert.isTrue(timingStub.calledOnce);
-      assert.equal(timingStub.lastCall.args[0], key);
-      assert.equal(timingStub.lastCall.args[1], 100);
-
-      nowStub.returns(350);
-      element.recordDraftInteraction();
-      assert.isTrue(timingStub.calledTwice);
-      assert.equal(timingStub.lastCall.args[0], key);
-      assert.equal(timingStub.lastCall.args[1], 150);
-
-      nowStub.returns(370 + 2 * 60 * 1000);
-      element.recordDraftInteraction();
-      assert.isFalse(timingStub.calledThrice);
-    });
-
-    test('timeEndWithAverage', () => {
-      const nowStub = sandbox.stub(element, 'now').returns(0);
-      nowStub.returns(1000);
-      element.time('foo');
-      nowStub.returns(1100);
-      element.timeEndWithAverage('foo', 'bar', 10);
-      assert.isTrue(element.reporter.calledTwice);
-      assert.isTrue(element.reporter.calledWithMatch(
-          'timing-report', 'UI Latency', 'foo', 100));
-      assert.isTrue(element.reporter.calledWithMatch(
-          'timing-report', 'UI Latency', 'bar', 10));
-    });
-
-    test('reportExtension', () => {
-      element.reportExtension('foo');
-      assert.isTrue(element.reporter.calledWithExactly(
-          'lifecycle', 'Extension detected', 'foo'
-      ));
-    });
-
-    test('reportInteraction', () => {
       element.reporter.restore();
-      sandbox.spy(element, '_reportEvent');
-      element.pluginsLoaded(); // so we don't cache
-      element.reportInteraction('button-click', {name: 'sendReply'});
-      assert.isTrue(element._reportEvent.getCall(2).calledWithMatch(
-          {
-            type: 'interaction',
-            name: 'button-click',
-            eventDetails: JSON.stringify({name: 'sendReply'}),
-          }
-      ));
+      sandbox.stub(element, '_reportEvent');
     });
 
-    test('report start time', () => {
-      element.reporter.restore();
+    test('pluginsLoaded reports time', () => {
       sandbox.stub(element, 'now').returns(42);
-      sandbox.spy(element, '_reportEvent');
-      const dispatchStub = sandbox.spy(document, 'dispatchEvent');
       element.pluginsLoaded();
-      element.time('timeAction');
-      element.timeEnd('timeAction');
-      assert.isTrue(element._reportEvent.getCall(2).calledWithMatch(
+      assert.isTrue(element._reportEvent.calledWithMatch(
           {
             type: 'timing-report',
             category: 'UI Latency',
-            name: 'timeAction',
-            value: 0,
-            eventStart: 42,
+            name: 'PluginsLoaded',
+            value: 42,
           }
       ));
-      assert.equal(dispatchStub.getCall(2).args[0].detail.eventStart, 42);
     });
 
-    suite('plugins', () => {
-      setup(() => {
-        element.reporter.restore();
-        sandbox.stub(element, '_reportEvent');
-      });
-
-      test('pluginsLoaded reports time', () => {
-        sandbox.stub(element, 'now').returns(42);
-        element.pluginsLoaded();
-        assert.isTrue(element._reportEvent.calledWithMatch(
-            {
-              type: 'timing-report',
-              category: 'UI Latency',
-              name: 'PluginsLoaded',
-              value: 42,
-            }
-        ));
-      });
-
-      test('pluginsLoaded reports plugins', () => {
-        element.pluginsLoaded(['foo', 'bar']);
-        assert.isTrue(element._reportEvent.calledWithMatch(
-            {
-              type: 'lifecycle',
-              category: 'Plugins installed',
-              eventDetails: JSON.stringify({pluginsList: ['foo', 'bar']}),
-            }
-        ));
-      });
-
-      test('caches reports if plugins are not loaded', () => {
-        element.timeEnd('foo');
-        assert.isFalse(element._reportEvent.called);
-      });
-
-      test('reports if plugins are loaded', () => {
-        element.pluginsLoaded();
-        assert.isTrue(element._reportEvent.called);
-      });
-
-      test('reports if metrics plugin xyz is loaded', () => {
-        element.pluginLoaded('metrics-xyz');
-        assert.isTrue(element._reportEvent.called);
-      });
-
-      test('reports cached events preserving order', () => {
-        element.time('foo');
-        element.time('bar');
-        element.timeEnd('foo');
-        element.pluginsLoaded();
-        element.timeEnd('bar');
-        assert.isTrue(element._reportEvent.getCall(0).calledWithMatch(
-            {type: 'timing-report', category: 'UI Latency', name: 'foo'}
-        ));
-        assert.isTrue(element._reportEvent.getCall(1).calledWithMatch(
-            {type: 'timing-report', category: 'UI Latency',
-              name: 'PluginsLoaded'}
-        ));
-        assert.isTrue(element._reportEvent.getCall(2).calledWithMatch(
-            {type: 'lifecycle', category: 'Plugins installed'}
-        ));
-        assert.isTrue(element._reportEvent.getCall(3).calledWithMatch(
-            {type: 'timing-report', category: 'UI Latency', name: 'bar'}
-        ));
-      });
+    test('pluginsLoaded reports plugins', () => {
+      element.pluginsLoaded(['foo', 'bar']);
+      assert.isTrue(element._reportEvent.calledWithMatch(
+          {
+            type: 'lifecycle',
+            category: 'Plugins installed',
+            eventDetails: JSON.stringify({pluginsList: ['foo', 'bar']}),
+          }
+      ));
     });
 
-    test('search', () => {
-      element.locationChanged('_handleSomeRoute');
-      assert.isTrue(element.reporter.calledWithExactly(
-          'nav-report', 'Location Changed', 'Page', '_handleSomeRoute'));
+    test('caches reports if plugins are not loaded', () => {
+      element.timeEnd('foo');
+      assert.isFalse(element._reportEvent.called);
     });
 
-    suite('exception logging', () => {
-      let fakeWindow;
-      let reporter;
+    test('reports if plugins are loaded', () => {
+      element.pluginsLoaded();
+      assert.isTrue(element._reportEvent.called);
+    });
 
-      const emulateThrow = function(msg, url, line, column, error) {
-        return fakeWindow.onerror(msg, url, line, column, error);
-      };
+    test('reports if metrics plugin xyz is loaded', () => {
+      element.pluginLoaded('metrics-xyz');
+      assert.isTrue(element._reportEvent.called);
+    });
 
-      setup(() => {
-        reporter = sandbox.stub(GrReporting.prototype, 'reporter');
-        fakeWindow = {
-          handlers: {},
-          addEventListener(type, handler) {
-            this.handlers[type] = handler;
-          },
-        };
-        sandbox.stub(console, 'error');
-        window.GrReporting._catchErrors(fakeWindow);
-      });
-
-      test('is reported', () => {
-        const error = new Error('bar');
-        error.stack = undefined;
-        emulateThrow('bar', 'http://url', 4, 2, error);
-        assert.isTrue(reporter.calledWith('error', 'exception', 'bar'));
-        const payload = reporter.lastCall.args[3];
-        assert.deepEqual(payload, {
-          url: 'http://url',
-          line: 4,
-          column: 2,
-          error,
-        });
-      });
-
-      test('is reported with 3 lines of stack', () => {
-        const error = new Error('bar');
-        emulateThrow('bar', 'http://url', 4, 2, error);
-        const expectedStack = error.stack.split('\n').slice(0, 3)
-            .join('\n');
-        assert.isTrue(reporter.calledWith('error', 'exception',
-            expectedStack));
-      });
-
-      test('prevent default event handler', () => {
-        assert.isTrue(emulateThrow());
-      });
-
-      test('unhandled rejection', () => {
-        fakeWindow.handlers['unhandledrejection']({
-          reason: {
-            message: 'bar',
-          },
-        });
-        assert.isTrue(reporter.calledWith('error', 'exception', 'bar'));
-      });
+    test('reports cached events preserving order', () => {
+      element.time('foo');
+      element.time('bar');
+      element.timeEnd('foo');
+      element.pluginsLoaded();
+      element.timeEnd('bar');
+      assert.isTrue(element._reportEvent.getCall(0).calledWithMatch(
+          {type: 'timing-report', category: 'UI Latency', name: 'foo'}
+      ));
+      assert.isTrue(element._reportEvent.getCall(1).calledWithMatch(
+          {type: 'timing-report', category: 'UI Latency',
+            name: 'PluginsLoaded'}
+      ));
+      assert.isTrue(element._reportEvent.getCall(2).calledWithMatch(
+          {type: 'lifecycle', category: 'Plugins installed'}
+      ));
+      assert.isTrue(element._reportEvent.getCall(3).calledWithMatch(
+          {type: 'timing-report', category: 'UI Latency', name: 'bar'}
+      ));
     });
   });
+
+  test('search', () => {
+    element.locationChanged('_handleSomeRoute');
+    assert.isTrue(element.reporter.calledWithExactly(
+        'nav-report', 'Location Changed', 'Page', '_handleSomeRoute'));
+  });
+
+  suite('exception logging', () => {
+    let fakeWindow;
+    let reporter;
+
+    const emulateThrow = function(msg, url, line, column, error) {
+      return fakeWindow.onerror(msg, url, line, column, error);
+    };
+
+    setup(() => {
+      reporter = sandbox.stub(GrReporting.prototype, 'reporter');
+      fakeWindow = {
+        handlers: {},
+        addEventListener(type, handler) {
+          this.handlers[type] = handler;
+        },
+      };
+      sandbox.stub(console, 'error');
+      window.GrReporting._catchErrors(fakeWindow);
+    });
+
+    test('is reported', () => {
+      const error = new Error('bar');
+      error.stack = undefined;
+      emulateThrow('bar', 'http://url', 4, 2, error);
+      assert.isTrue(reporter.calledWith('error', 'exception', 'bar'));
+      const payload = reporter.lastCall.args[3];
+      assert.deepEqual(payload, {
+        url: 'http://url',
+        line: 4,
+        column: 2,
+        error,
+      });
+    });
+
+    test('is reported with 3 lines of stack', () => {
+      const error = new Error('bar');
+      emulateThrow('bar', 'http://url', 4, 2, error);
+      const expectedStack = error.stack.split('\n').slice(0, 3)
+          .join('\n');
+      assert.isTrue(reporter.calledWith('error', 'exception',
+          expectedStack));
+    });
+
+    test('prevent default event handler', () => {
+      assert.isTrue(emulateThrow());
+    });
+
+    test('unhandled rejection', () => {
+      fakeWindow.handlers['unhandledrejection']({
+        reason: {
+          message: 'bar',
+        },
+      });
+      assert.isTrue(reporter.calledWith('error', 'exception', 'bar'));
+    });
+  });
+});
 </script>
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.js b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
index ebac1e1..e461d1d 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -14,1520 +14,1535 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-(function() {
-  'use strict';
+import '../../../scripts/bundled-polymer.js';
 
-  const RoutePattern = {
-    ROOT: '/',
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
+import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
+import '../gr-navigation/gr-navigation.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-reporting/gr-reporting.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import page from 'page/page.mjs';
+self.page = page;
+import {htmlTemplate} from './gr-router_html.js';
 
-    DASHBOARD: /^\/dashboard\/(.+)$/,
-    CUSTOM_DASHBOARD: /^\/dashboard\/?$/,
-    PROJECT_DASHBOARD: /^\/p\/(.+)\/\+\/dashboard\/(.+)/,
+const RoutePattern = {
+  ROOT: '/',
 
-    AGREEMENTS: /^\/settings\/agreements\/?/,
-    NEW_AGREEMENTS: /^\/settings\/new-agreement\/?/,
-    REGISTER: /^\/register(\/.*)?$/,
+  DASHBOARD: /^\/dashboard\/(.+)$/,
+  CUSTOM_DASHBOARD: /^\/dashboard\/?$/,
+  PROJECT_DASHBOARD: /^\/p\/(.+)\/\+\/dashboard\/(.+)/,
 
-    // Pattern for login and logout URLs intended to be passed-through. May
-    // include a return URL.
-    LOG_IN_OR_OUT: /\/log(in|out)(\/(.+))?$/,
+  AGREEMENTS: /^\/settings\/agreements\/?/,
+  NEW_AGREEMENTS: /^\/settings\/new-agreement\/?/,
+  REGISTER: /^\/register(\/.*)?$/,
 
-    // Pattern for a catchall route when no other pattern is matched.
-    DEFAULT: /.*/,
+  // Pattern for login and logout URLs intended to be passed-through. May
+  // include a return URL.
+  LOG_IN_OR_OUT: /\/log(in|out)(\/(.+))?$/,
 
-    // Matches /admin/groups/[uuid-]<group>
-    GROUP: /^\/admin\/groups\/(?:uuid-)?([^,]+)$/,
+  // Pattern for a catchall route when no other pattern is matched.
+  DEFAULT: /.*/,
 
-    // Redirects /groups/self to /settings/#Groups for GWT compatibility
-    GROUP_SELF: /^\/groups\/self/,
+  // Matches /admin/groups/[uuid-]<group>
+  GROUP: /^\/admin\/groups\/(?:uuid-)?([^,]+)$/,
 
-    // Matches /admin/groups/[uuid-]<group>,info (backwords compat with gwtui)
-    // Redirects to /admin/groups/[uuid-]<group>
-    GROUP_INFO: /^\/admin\/groups\/(?:uuid-)?(.+),info$/,
+  // Redirects /groups/self to /settings/#Groups for GWT compatibility
+  GROUP_SELF: /^\/groups\/self/,
 
-    // Matches /admin/groups/<group>,audit-log
-    GROUP_AUDIT_LOG: /^\/admin\/groups\/(?:uuid-)?(.+),audit-log$/,
+  // Matches /admin/groups/[uuid-]<group>,info (backwords compat with gwtui)
+  // Redirects to /admin/groups/[uuid-]<group>
+  GROUP_INFO: /^\/admin\/groups\/(?:uuid-)?(.+),info$/,
 
-    // Matches /admin/groups/[uuid-]<group>,members
-    GROUP_MEMBERS: /^\/admin\/groups\/(?:uuid-)?(.+),members$/,
+  // Matches /admin/groups/<group>,audit-log
+  GROUP_AUDIT_LOG: /^\/admin\/groups\/(?:uuid-)?(.+),audit-log$/,
 
-    // Matches /admin/groups[,<offset>][/].
-    GROUP_LIST_OFFSET: /^\/admin\/groups(,(\d+))?(\/)?$/,
-    GROUP_LIST_FILTER: '/admin/groups/q/filter::filter',
-    GROUP_LIST_FILTER_OFFSET: '/admin/groups/q/filter::filter,:offset',
+  // Matches /admin/groups/[uuid-]<group>,members
+  GROUP_MEMBERS: /^\/admin\/groups\/(?:uuid-)?(.+),members$/,
 
-    // Matches /admin/create-project
-    LEGACY_CREATE_PROJECT: /^\/admin\/create-project\/?$/,
+  // Matches /admin/groups[,<offset>][/].
+  GROUP_LIST_OFFSET: /^\/admin\/groups(,(\d+))?(\/)?$/,
+  GROUP_LIST_FILTER: '/admin/groups/q/filter::filter',
+  GROUP_LIST_FILTER_OFFSET: '/admin/groups/q/filter::filter,:offset',
 
-    // Matches /admin/create-project
-    LEGACY_CREATE_GROUP: /^\/admin\/create-group\/?$/,
+  // Matches /admin/create-project
+  LEGACY_CREATE_PROJECT: /^\/admin\/create-project\/?$/,
 
-    PROJECT_OLD: /^\/admin\/(projects)\/?(.+)?$/,
+  // Matches /admin/create-project
+  LEGACY_CREATE_GROUP: /^\/admin\/create-group\/?$/,
 
-    // Matches /admin/repos/<repo>
-    REPO: /^\/admin\/repos\/([^,]+)$/,
+  PROJECT_OLD: /^\/admin\/(projects)\/?(.+)?$/,
 
-    // Matches /admin/repos/<repo>,commands.
-    REPO_COMMANDS: /^\/admin\/repos\/(.+),commands$/,
+  // Matches /admin/repos/<repo>
+  REPO: /^\/admin\/repos\/([^,]+)$/,
 
-    // Matches /admin/repos/<repos>,access.
-    REPO_ACCESS: /^\/admin\/repos\/(.+),access$/,
+  // Matches /admin/repos/<repo>,commands.
+  REPO_COMMANDS: /^\/admin\/repos\/(.+),commands$/,
 
-    // Matches /admin/repos/<repos>,access.
-    REPO_DASHBOARDS: /^\/admin\/repos\/(.+),dashboards$/,
+  // Matches /admin/repos/<repos>,access.
+  REPO_ACCESS: /^\/admin\/repos\/(.+),access$/,
 
-    // Matches /admin/repos[,<offset>][/].
-    REPO_LIST_OFFSET: /^\/admin\/repos(,(\d+))?(\/)?$/,
-    REPO_LIST_FILTER: '/admin/repos/q/filter::filter',
-    REPO_LIST_FILTER_OFFSET: '/admin/repos/q/filter::filter,:offset',
+  // Matches /admin/repos/<repos>,access.
+  REPO_DASHBOARDS: /^\/admin\/repos\/(.+),dashboards$/,
 
-    // Matches /admin/repos/<repo>,branches[,<offset>].
-    BRANCH_LIST_OFFSET: /^\/admin\/repos\/(.+),branches(,(.+))?$/,
-    BRANCH_LIST_FILTER: '/admin/repos/:repo,branches/q/filter::filter',
-    BRANCH_LIST_FILTER_OFFSET:
-        '/admin/repos/:repo,branches/q/filter::filter,:offset',
+  // Matches /admin/repos[,<offset>][/].
+  REPO_LIST_OFFSET: /^\/admin\/repos(,(\d+))?(\/)?$/,
+  REPO_LIST_FILTER: '/admin/repos/q/filter::filter',
+  REPO_LIST_FILTER_OFFSET: '/admin/repos/q/filter::filter,:offset',
 
-    // Matches /admin/repos/<repo>,tags[,<offset>].
-    TAG_LIST_OFFSET: /^\/admin\/repos\/(.+),tags(,(.+))?$/,
-    TAG_LIST_FILTER: '/admin/repos/:repo,tags/q/filter::filter',
-    TAG_LIST_FILTER_OFFSET:
-        '/admin/repos/:repo,tags/q/filter::filter,:offset',
+  // Matches /admin/repos/<repo>,branches[,<offset>].
+  BRANCH_LIST_OFFSET: /^\/admin\/repos\/(.+),branches(,(.+))?$/,
+  BRANCH_LIST_FILTER: '/admin/repos/:repo,branches/q/filter::filter',
+  BRANCH_LIST_FILTER_OFFSET:
+      '/admin/repos/:repo,branches/q/filter::filter,:offset',
 
-    PLUGINS: /^\/plugins\/(.+)$/,
+  // Matches /admin/repos/<repo>,tags[,<offset>].
+  TAG_LIST_OFFSET: /^\/admin\/repos\/(.+),tags(,(.+))?$/,
+  TAG_LIST_FILTER: '/admin/repos/:repo,tags/q/filter::filter',
+  TAG_LIST_FILTER_OFFSET:
+      '/admin/repos/:repo,tags/q/filter::filter,:offset',
 
-    PLUGIN_LIST: /^\/admin\/plugins(\/)?$/,
+  PLUGINS: /^\/plugins\/(.+)$/,
 
-    // Matches /admin/plugins[,<offset>][/].
-    PLUGIN_LIST_OFFSET: /^\/admin\/plugins(,(\d+))?(\/)?$/,
-    PLUGIN_LIST_FILTER: '/admin/plugins/q/filter::filter',
-    PLUGIN_LIST_FILTER_OFFSET: '/admin/plugins/q/filter::filter,:offset',
+  PLUGIN_LIST: /^\/admin\/plugins(\/)?$/,
 
-    QUERY: /^\/q\/([^,]+)(,(\d+))?$/,
+  // Matches /admin/plugins[,<offset>][/].
+  PLUGIN_LIST_OFFSET: /^\/admin\/plugins(,(\d+))?(\/)?$/,
+  PLUGIN_LIST_FILTER: '/admin/plugins/q/filter::filter',
+  PLUGIN_LIST_FILTER_OFFSET: '/admin/plugins/q/filter::filter,:offset',
 
-    /**
-     * Support vestigial params from GWT UI.
-     *
-     * @see Issue 7673.
-     * @type {!RegExp}
-     */
-    QUERY_LEGACY_SUFFIX: /^\/q\/.+,n,z$/,
-
-    // Matches /c/<changeNum>/[<basePatchNum>..][<patchNum>][/].
-    CHANGE_LEGACY: /^\/c\/(\d+)\/?(((-?\d+|edit)(\.\.(\d+|edit))?))?\/?$/,
-    CHANGE_NUMBER_LEGACY: /^\/(\d+)\/?/,
-
-    // Matches
-    // /c/<project>/+/<changeNum>/[<basePatchNum|edit>..][<patchNum|edit>].
-    // TODO(kaspern): Migrate completely to project based URLs, with backwards
-    // compatibility for change-only.
-    CHANGE: /^\/c\/(.+)\/\+\/(\d+)(\/?((-?\d+|edit)(\.\.(\d+|edit))?))?\/?$/,
-
-    // Matches /c/<project>/+/<changeNum>/[<patchNum|edit>],edit
-    CHANGE_EDIT: /^\/c\/(.+)\/\+\/(\d+)(\/(\d+))?,edit\/?$/,
-
-    // Matches
-    // /c/<project>/+/<changeNum>/[<basePatchNum|edit>..]<patchNum|edit>/<path>.
-    // TODO(kaspern): Migrate completely to project based URLs, with backwards
-    // compatibility for change-only.
-    // eslint-disable-next-line max-len
-    DIFF: /^\/c\/(.+)\/\+\/(\d+)(\/((-?\d+|edit)(\.\.(\d+|edit))?(\/(.+))))\/?$/,
-
-    // Matches /c/<project>/+/<changeNum>/[<patchNum|edit>]/<path>,edit[#lineNum]
-    DIFF_EDIT: /^\/c\/(.+)\/\+\/(\d+)\/(\d+|edit)\/(.+),edit(\#\d+)?$/,
-
-    // Matches non-project-relative
-    // /c/<changeNum>/[<basePatchNum>..]<patchNum>/<path>.
-    DIFF_LEGACY: /^\/c\/(\d+)\/((-?\d+|edit)(\.\.(\d+|edit))?)\/(.+)/,
-
-    // Matches diff routes using @\d+ to specify a file name (whether or not
-    // the project name is included).
-    // eslint-disable-next-line max-len
-    DIFF_LEGACY_LINENUM: /^\/c\/((.+)\/\+\/)?(\d+)(\/?((-?\d+|edit)(\.\.(\d+|edit))?\/(.+))?)@[ab]?\d+$/,
-
-    SETTINGS: /^\/settings\/?/,
-    SETTINGS_LEGACY: /^\/settings\/VE\/(\S+)/,
-
-    // Matches /c/<changeNum>/ /<URL tail>
-    // Catches improperly encoded URLs (context: Issue 7100)
-    IMPROPERLY_ENCODED_PLUS: /^\/c\/(.+)\/\ \/(.+)$/,
-
-    PLUGIN_SCREEN: /^\/x\/([\w-]+)\/([\w-]+)\/?/,
-
-    DOCUMENTATION_SEARCH_FILTER: '/Documentation/q/filter::filter',
-    DOCUMENTATION_SEARCH: /^\/Documentation\/q\/(.*)$/,
-    DOCUMENTATION: /^\/Documentation(\/)?(.+)?/,
-  };
+  QUERY: /^\/q\/([^,]+)(,(\d+))?$/,
 
   /**
-   * Pattern to recognize and parse the diff line locations as they appear in
-   * the hash of diff URLs. In this format, a number on its own indicates that
-   * line number in the revision of the diff. A number prefixed by either an 'a'
-   * or a 'b' indicates that line number of the base of the diff.
+   * Support vestigial params from GWT UI.
    *
-   * @type {RegExp}
+   * @see Issue 7673.
+   * @type {!RegExp}
    */
-  const LINE_ADDRESS_PATTERN = /^([ab]?)(\d+)$/;
+  QUERY_LEGACY_SUFFIX: /^\/q\/.+,n,z$/,
 
-  /**
-   * Pattern to recognize '+' in url-encoded strings for replacement with ' '.
-   */
-  const PLUS_PATTERN = /\+/g;
+  // Matches /c/<changeNum>/[<basePatchNum>..][<patchNum>][/].
+  CHANGE_LEGACY: /^\/c\/(\d+)\/?(((-?\d+|edit)(\.\.(\d+|edit))?))?\/?$/,
+  CHANGE_NUMBER_LEGACY: /^\/(\d+)\/?/,
 
-  /**
-   * Pattern to recognize leading '?' in window.location.search, for stripping.
-   */
-  const QUESTION_PATTERN = /^\?*/;
+  // Matches
+  // /c/<project>/+/<changeNum>/[<basePatchNum|edit>..][<patchNum|edit>].
+  // TODO(kaspern): Migrate completely to project based URLs, with backwards
+  // compatibility for change-only.
+  CHANGE: /^\/c\/(.+)\/\+\/(\d+)(\/?((-?\d+|edit)(\.\.(\d+|edit))?))?\/?$/,
 
-  /**
-   * GWT UI would use @\d+ at the end of a path to indicate linenum.
-   */
-  const LEGACY_LINENUM_PATTERN = /@([ab]?\d+)$/;
+  // Matches /c/<project>/+/<changeNum>/[<patchNum|edit>],edit
+  CHANGE_EDIT: /^\/c\/(.+)\/\+\/(\d+)(\/(\d+))?,edit\/?$/,
 
-  const LEGACY_QUERY_SUFFIX_PATTERN = /,n,z$/;
+  // Matches
+  // /c/<project>/+/<changeNum>/[<basePatchNum|edit>..]<patchNum|edit>/<path>.
+  // TODO(kaspern): Migrate completely to project based URLs, with backwards
+  // compatibility for change-only.
+  // eslint-disable-next-line max-len
+  DIFF: /^\/c\/(.+)\/\+\/(\d+)(\/((-?\d+|edit)(\.\.(\d+|edit))?(\/(.+))))\/?$/,
 
-  const REPO_TOKEN_PATTERN = /\$\{(project|repo)\}/g;
+  // Matches /c/<project>/+/<changeNum>/[<patchNum|edit>]/<path>,edit[#lineNum]
+  DIFF_EDIT: /^\/c\/(.+)\/\+\/(\d+)\/(\d+|edit)\/(.+),edit(\#\d+)?$/,
 
-  // Polymer makes `app` intrinsically defined on the window by virtue of the
-  // custom element having the id "app", but it is made explicit here.
-  const app = document.querySelector('#app');
-  if (!app) {
-    console.log('No gr-app found (running tests)');
+  // Matches non-project-relative
+  // /c/<changeNum>/[<basePatchNum>..]<patchNum>/<path>.
+  DIFF_LEGACY: /^\/c\/(\d+)\/((-?\d+|edit)(\.\.(\d+|edit))?)\/(.+)/,
+
+  // Matches diff routes using @\d+ to specify a file name (whether or not
+  // the project name is included).
+  // eslint-disable-next-line max-len
+  DIFF_LEGACY_LINENUM: /^\/c\/((.+)\/\+\/)?(\d+)(\/?((-?\d+|edit)(\.\.(\d+|edit))?\/(.+))?)@[ab]?\d+$/,
+
+  SETTINGS: /^\/settings\/?/,
+  SETTINGS_LEGACY: /^\/settings\/VE\/(\S+)/,
+
+  // Matches /c/<changeNum>/ /<URL tail>
+  // Catches improperly encoded URLs (context: Issue 7100)
+  IMPROPERLY_ENCODED_PLUS: /^\/c\/(.+)\/\ \/(.+)$/,
+
+  PLUGIN_SCREEN: /^\/x\/([\w-]+)\/([\w-]+)\/?/,
+
+  DOCUMENTATION_SEARCH_FILTER: '/Documentation/q/filter::filter',
+  DOCUMENTATION_SEARCH: /^\/Documentation\/q\/(.*)$/,
+  DOCUMENTATION: /^\/Documentation(\/)?(.+)?/,
+};
+
+/**
+ * Pattern to recognize and parse the diff line locations as they appear in
+ * the hash of diff URLs. In this format, a number on its own indicates that
+ * line number in the revision of the diff. A number prefixed by either an 'a'
+ * or a 'b' indicates that line number of the base of the diff.
+ *
+ * @type {RegExp}
+ */
+const LINE_ADDRESS_PATTERN = /^([ab]?)(\d+)$/;
+
+/**
+ * Pattern to recognize '+' in url-encoded strings for replacement with ' '.
+ */
+const PLUS_PATTERN = /\+/g;
+
+/**
+ * Pattern to recognize leading '?' in window.location.search, for stripping.
+ */
+const QUESTION_PATTERN = /^\?*/;
+
+/**
+ * GWT UI would use @\d+ at the end of a path to indicate linenum.
+ */
+const LEGACY_LINENUM_PATTERN = /@([ab]?\d+)$/;
+
+const LEGACY_QUERY_SUFFIX_PATTERN = /,n,z$/;
+
+const REPO_TOKEN_PATTERN = /\$\{(project|repo)\}/g;
+
+// Polymer makes `app` intrinsically defined on the window by virtue of the
+// custom element having the id "app", but it is made explicit here.
+const app = document.querySelector('#app');
+if (!app) {
+  console.log('No gr-app found (running tests)');
+}
+
+// Setup listeners outside of the router component initialization.
+(function() {
+  const reporting = document.createElement('gr-reporting');
+
+  window.addEventListener('WebComponentsReady', () => {
+    reporting.timeEnd('WebComponentsReady');
+  });
+})();
+
+/**
+ * @appliesMixin Gerrit.BaseUrlMixin
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.PatchSetMixin
+ * @appliesMixin Gerrit.URLEncodingMixin
+ * @extends Polymer.Element
+ */
+class GrRouter extends mixinBehaviors( [
+  Gerrit.BaseUrlBehavior,
+  Gerrit.FireBehavior,
+  Gerrit.PatchSetBehavior,
+  Gerrit.URLEncodingBehavior,
+], GestureEventListeners(
+    LegacyElementMixin(
+        PolymerElement))) {
+  static get template() { return htmlTemplate; }
+
+  static get is() { return 'gr-router'; }
+
+  static get properties() {
+    return {
+      _app: {
+        type: Object,
+        value: app,
+      },
+      _isRedirecting: Boolean,
+      // This variable is to differentiate between internal navigation (false)
+      // and for first navigation in app after loaded from server (true).
+      _isInitialLoad: {
+        type: Boolean,
+        value: true,
+      },
+    };
   }
 
-  // Setup listeners outside of the router component initialization.
-  (function() {
-    const reporting = document.createElement('gr-reporting');
+  start() {
+    if (!this._app) { return; }
+    this._startRouter();
+  }
 
-    window.addEventListener('WebComponentsReady', () => {
-      reporting.timeEnd('WebComponentsReady');
-    });
-  })();
+  _setParams(params) {
+    this._appElement().params = params;
+  }
+
+  _appElement() {
+    // In Polymer2 you have to reach through the shadow root of the app
+    // element. This obviously breaks encapsulation.
+    // TODO(brohlfs): Make this more elegant, e.g. by exposing app-element
+    // explicitly in app, or by delegating to it.
+    return document.getElementById('app-element') ||
+        document.getElementById('app').shadowRoot.getElementById(
+            'app-element');
+  }
+
+  _redirect(url) {
+    this._isRedirecting = true;
+    page.redirect(url);
+  }
 
   /**
-   * @appliesMixin Gerrit.BaseUrlMixin
-   * @appliesMixin Gerrit.FireMixin
-   * @appliesMixin Gerrit.PatchSetMixin
-   * @appliesMixin Gerrit.URLEncodingMixin
-   * @extends Polymer.Element
+   * @param {!Object} params
+   * @return {string}
    */
-  class GrRouter extends Polymer.mixinBehaviors( [
-    Gerrit.BaseUrlBehavior,
-    Gerrit.FireBehavior,
-    Gerrit.PatchSetBehavior,
-    Gerrit.URLEncodingBehavior,
-  ], Polymer.GestureEventListeners(
-      Polymer.LegacyElementMixin(
-          Polymer.Element))) {
-    static get is() { return 'gr-router'; }
+  _generateUrl(params) {
+    const base = this.getBaseUrl();
+    let url = '';
+    const Views = Gerrit.Nav.View;
 
-    static get properties() {
-      return {
-        _app: {
-          type: Object,
-          value: app,
-        },
-        _isRedirecting: Boolean,
-        // This variable is to differentiate between internal navigation (false)
-        // and for first navigation in app after loaded from server (true).
-        _isInitialLoad: {
-          type: Boolean,
-          value: true,
-        },
-      };
+    if (params.view === Views.SEARCH) {
+      url = this._generateSearchUrl(params);
+    } else if (params.view === Views.CHANGE) {
+      url = this._generateChangeUrl(params);
+    } else if (params.view === Views.DASHBOARD) {
+      url = this._generateDashboardUrl(params);
+    } else if (params.view === Views.DIFF || params.view === Views.EDIT) {
+      url = this._generateDiffOrEditUrl(params);
+    } else if (params.view === Views.GROUP) {
+      url = this._generateGroupUrl(params);
+    } else if (params.view === Views.REPO) {
+      url = this._generateRepoUrl(params);
+    } else if (params.view === Views.ROOT) {
+      url = '/';
+    } else if (params.view === Views.SETTINGS) {
+      url = this._generateSettingsUrl(params);
+    } else {
+      throw new Error('Can\'t generate');
     }
 
-    start() {
-      if (!this._app) { return; }
-      this._startRouter();
+    return base + url;
+  }
+
+  _generateWeblinks(params) {
+    const type = params.type;
+    switch (type) {
+      case Gerrit.Nav.WeblinkType.FILE:
+        return this._getFileWebLinks(params);
+      case Gerrit.Nav.WeblinkType.CHANGE:
+        return this._getChangeWeblinks(params);
+      case Gerrit.Nav.WeblinkType.PATCHSET:
+        return this._getPatchSetWeblink(params);
+      default:
+        console.warn(`Unsupported weblink ${type}!`);
+    }
+  }
+
+  _getPatchSetWeblink(params) {
+    const {commit, options} = params;
+    const {weblinks, config} = options || {};
+    const name = commit && commit.slice(0, 7);
+    const weblink = this._getBrowseCommitWeblink(weblinks, config);
+    if (!weblink || !weblink.url) {
+      return {name};
+    } else {
+      return {name, url: weblink.url};
+    }
+  }
+
+  _firstCodeBrowserWeblink(weblinks) {
+    // This is an ordered whitelist of web link types that provide direct
+    // links to the commit in the url property.
+    const codeBrowserLinks = ['gitiles', 'browse', 'gitweb'];
+    for (let i = 0; i < codeBrowserLinks.length; i++) {
+      const weblink =
+        weblinks.find(weblink => weblink.name === codeBrowserLinks[i]);
+      if (weblink) { return weblink; }
+    }
+    return null;
+  }
+
+  _getBrowseCommitWeblink(weblinks, config) {
+    if (!weblinks) { return null; }
+    let weblink;
+    // Use primary weblink if configured and exists.
+    if (config && config.gerrit && config.gerrit.primary_weblink_name) {
+      weblink = weblinks.find(
+          weblink => weblink.name === config.gerrit.primary_weblink_name
+      );
+    }
+    if (!weblink) {
+      weblink = this._firstCodeBrowserWeblink(weblinks);
+    }
+    if (!weblink) { return null; }
+    return weblink;
+  }
+
+  _getChangeWeblinks({repo, commit, options: {weblinks, config}}) {
+    if (!weblinks || !weblinks.length) return [];
+    const commitWeblink = this._getBrowseCommitWeblink(weblinks, config);
+    return weblinks.filter(weblink =>
+      !commitWeblink ||
+      !commitWeblink.name ||
+      weblink.name !== commitWeblink.name);
+  }
+
+  _getFileWebLinks({repo, commit, file, options: {weblinks}}) {
+    return weblinks;
+  }
+
+  /**
+   * @param {!Object} params
+   * @return {string}
+   */
+  _generateSearchUrl(params) {
+    let offsetExpr = '';
+    if (params.offset && params.offset > 0) {
+      offsetExpr = ',' + params.offset;
     }
 
-    _setParams(params) {
-      this._appElement().params = params;
+    if (params.query) {
+      return '/q/' + this.encodeURL(params.query, true) + offsetExpr;
     }
 
-    _appElement() {
-      // In Polymer2 you have to reach through the shadow root of the app
-      // element. This obviously breaks encapsulation.
-      // TODO(brohlfs): Make this more elegant, e.g. by exposing app-element
-      // explicitly in app, or by delegating to it.
-      return document.getElementById('app-element') ||
-          document.getElementById('app').shadowRoot.getElementById(
-              'app-element');
+    const operators = [];
+    if (params.owner) {
+      operators.push('owner:' + this.encodeURL(params.owner, false));
+    }
+    if (params.project) {
+      operators.push('project:' + this.encodeURL(params.project, false));
+    }
+    if (params.branch) {
+      operators.push('branch:' + this.encodeURL(params.branch, false));
+    }
+    if (params.topic) {
+      operators.push('topic:"' + this.encodeURL(params.topic, false) + '"');
+    }
+    if (params.hashtag) {
+      operators.push('hashtag:"' +
+          this.encodeURL(params.hashtag.toLowerCase(), false) + '"');
+    }
+    if (params.statuses) {
+      if (params.statuses.length === 1) {
+        operators.push(
+            'status:' + this.encodeURL(params.statuses[0], false));
+      } else if (params.statuses.length > 1) {
+        operators.push(
+            '(' +
+            params.statuses.map(s => `status:${this.encodeURL(s, false)}`)
+                .join(' OR ') +
+            ')');
+      }
     }
 
-    _redirect(url) {
-      this._isRedirecting = true;
-      page.redirect(url);
+    return '/q/' + operators.join('+') + offsetExpr;
+  }
+
+  /**
+   * @param {!Object} params
+   * @return {string}
+   */
+  _generateChangeUrl(params) {
+    let range = this._getPatchRangeExpression(params);
+    if (range.length) { range = '/' + range; }
+    let suffix = `${range}`;
+    if (params.querystring) {
+      suffix += '?' + params.querystring;
+    } else if (params.edit) {
+      suffix += ',edit';
+    }
+    if (params.messageHash) {
+      suffix += params.messageHash;
+    }
+    if (params.project) {
+      const encodedProject = this.encodeURL(params.project, true);
+      return `/c/${encodedProject}/+/${params.changeNum}${suffix}`;
+    } else {
+      return `/c/${params.changeNum}${suffix}`;
+    }
+  }
+
+  /**
+   * @param {!Object} params
+   * @return {string}
+   */
+  _generateDashboardUrl(params) {
+    const repoName = params.repo || params.project || null;
+    if (params.sections) {
+      // Custom dashboard.
+      const queryParams = this._sectionsToEncodedParams(params.sections,
+          repoName);
+      if (params.title) {
+        queryParams.push('title=' + encodeURIComponent(params.title));
+      }
+      const user = params.user ? params.user : '';
+      return `/dashboard/${user}?${queryParams.join('&')}`;
+    } else if (repoName) {
+      // Project dashboard.
+      const encodedRepo = this.encodeURL(repoName, true);
+      return `/p/${encodedRepo}/+/dashboard/${params.dashboard}`;
+    } else {
+      // User dashboard.
+      return `/dashboard/${params.user || 'self'}`;
+    }
+  }
+
+  /**
+   * @param {!Array<!{name: string, query: string}>} sections
+   * @param {string=} opt_repoName
+   * @return {!Array<string>}
+   */
+  _sectionsToEncodedParams(sections, opt_repoName) {
+    return sections.map(section => {
+      // If there is a repo name provided, make sure to substitute it into the
+      // ${repo} (or legacy ${project}) query tokens.
+      const query = opt_repoName ?
+        section.query.replace(REPO_TOKEN_PATTERN, opt_repoName) :
+        section.query;
+      return encodeURIComponent(section.name) + '=' +
+          encodeURIComponent(query);
+    });
+  }
+
+  /**
+   * @param {!Object} params
+   * @return {string}
+   */
+  _generateDiffOrEditUrl(params) {
+    let range = this._getPatchRangeExpression(params);
+    if (range.length) { range = '/' + range; }
+
+    let suffix = `${range}/${this.encodeURL(params.path, true)}`;
+
+    if (params.view === Gerrit.Nav.View.EDIT) { suffix += ',edit'; }
+
+    if (params.lineNum) {
+      suffix += '#';
+      if (params.leftSide) { suffix += 'b'; }
+      suffix += params.lineNum;
     }
 
-    /**
-     * @param {!Object} params
-     * @return {string}
-     */
-    _generateUrl(params) {
-      const base = this.getBaseUrl();
-      let url = '';
-      const Views = Gerrit.Nav.View;
+    if (params.project) {
+      const encodedProject = this.encodeURL(params.project, true);
+      return `/c/${encodedProject}/+/${params.changeNum}${suffix}`;
+    } else {
+      return `/c/${params.changeNum}${suffix}`;
+    }
+  }
 
-      if (params.view === Views.SEARCH) {
-        url = this._generateSearchUrl(params);
-      } else if (params.view === Views.CHANGE) {
-        url = this._generateChangeUrl(params);
-      } else if (params.view === Views.DASHBOARD) {
-        url = this._generateDashboardUrl(params);
-      } else if (params.view === Views.DIFF || params.view === Views.EDIT) {
-        url = this._generateDiffOrEditUrl(params);
-      } else if (params.view === Views.GROUP) {
-        url = this._generateGroupUrl(params);
-      } else if (params.view === Views.REPO) {
-        url = this._generateRepoUrl(params);
-      } else if (params.view === Views.ROOT) {
-        url = '/';
-      } else if (params.view === Views.SETTINGS) {
-        url = this._generateSettingsUrl(params);
+  /**
+   * @param {!Object} params
+   * @return {string}
+   */
+  _generateGroupUrl(params) {
+    let url = `/admin/groups/${this.encodeURL(params.groupId + '', true)}`;
+    if (params.detail === Gerrit.Nav.GroupDetailView.MEMBERS) {
+      url += ',members';
+    } else if (params.detail === Gerrit.Nav.GroupDetailView.LOG) {
+      url += ',audit-log';
+    }
+    return url;
+  }
+
+  /**
+   * @param {!Object} params
+   * @return {string}
+   */
+  _generateRepoUrl(params) {
+    let url = `/admin/repos/${this.encodeURL(params.repoName + '', true)}`;
+    if (params.detail === Gerrit.Nav.RepoDetailView.ACCESS) {
+      url += ',access';
+    } else if (params.detail === Gerrit.Nav.RepoDetailView.BRANCHES) {
+      url += ',branches';
+    } else if (params.detail === Gerrit.Nav.RepoDetailView.TAGS) {
+      url += ',tags';
+    } else if (params.detail === Gerrit.Nav.RepoDetailView.COMMANDS) {
+      url += ',commands';
+    } else if (params.detail === Gerrit.Nav.RepoDetailView.DASHBOARDS) {
+      url += ',dashboards';
+    }
+    return url;
+  }
+
+  /**
+   * @param {!Object} params
+   * @return {string}
+   */
+  _generateSettingsUrl(params) {
+    return '/settings';
+  }
+
+  /**
+   * Given an object of parameters, potentially including a `patchNum` or a
+   * `basePatchNum` or both, return a string representation of that range. If
+   * no range is indicated in the params, the empty string is returned.
+   *
+   * @param {!Object} params
+   * @return {string}
+   */
+  _getPatchRangeExpression(params) {
+    let range = '';
+    if (params.patchNum) { range = '' + params.patchNum; }
+    if (params.basePatchNum) { range = params.basePatchNum + '..' + range; }
+    return range;
+  }
+
+  /**
+   * Given a set of params without a project, gets the project from the rest
+   * API project lookup and then sets the app params.
+   *
+   * @param {?Object} params
+   */
+  _normalizeLegacyRouteParams(params) {
+    if (!params.changeNum) { return Promise.resolve(); }
+
+    return this.$.restAPI.getFromProjectLookup(params.changeNum)
+        .then(project => {
+          // Show a 404 and terminate if the lookup request failed. Attempting
+          // to redirect after failing to get the project loops infinitely.
+          if (!project) {
+            this._show404();
+            return;
+          }
+
+          params.project = project;
+          this._normalizePatchRangeParams(params);
+          this._redirect(this._generateUrl(params));
+        });
+  }
+
+  /**
+   * Normalizes the params object, and determines if the URL needs to be
+   * modified to fit the proper schema.
+   *
+   * @param {*} params
+   * @return {boolean} whether or not the URL needs to be upgraded.
+   */
+  _normalizePatchRangeParams(params) {
+    const hasBasePatchNum = params.basePatchNum !== null &&
+        params.basePatchNum !== undefined;
+    const hasPatchNum = params.patchNum !== null &&
+        params.patchNum !== undefined;
+    let needsRedirect = false;
+
+    // Diffing a patch against itself is invalid, so if the base and revision
+    // patches are equal clear the base.
+    if (hasBasePatchNum &&
+        this.patchNumEquals(params.basePatchNum, params.patchNum)) {
+      needsRedirect = true;
+      params.basePatchNum = null;
+    } else if (hasBasePatchNum && !hasPatchNum) {
+      // Regexes set basePatchNum instead of patchNum when only one is
+      // specified. Redirect is not needed in this case.
+      params.patchNum = params.basePatchNum;
+      params.basePatchNum = null;
+    }
+    return needsRedirect;
+  }
+
+  /**
+   * Redirect the user to login using the given return-URL for redirection
+   * after authentication success.
+   *
+   * @param {string} returnUrl
+   */
+  _redirectToLogin(returnUrl) {
+    const basePath = this.getBaseUrl() || '';
+    page(
+        '/login/' + encodeURIComponent(returnUrl.substring(basePath.length)));
+  }
+
+  /**
+   * Hashes parsed by page.js exclude "inner" hashes, so a URL like "/a#b#c"
+   * is parsed to have a hash of "b" rather than "b#c". Instead, this method
+   * parses hashes correctly. Will return an empty string if there is no hash.
+   *
+   * @param {!string} canonicalPath
+   * @return {!string} Everything after the first '#' ("a#b#c" -> "b#c").
+   */
+  _getHashFromCanonicalPath(canonicalPath) {
+    return canonicalPath.split('#').slice(1)
+        .join('#');
+  }
+
+  _parseLineAddress(hash) {
+    const match = hash.match(LINE_ADDRESS_PATTERN);
+    if (!match) { return null; }
+    return {
+      leftSide: !!match[1],
+      lineNum: parseInt(match[2], 10),
+    };
+  }
+
+  /**
+   * Check to see if the user is logged in and return a promise that only
+   * resolves if the user is logged in. If the user us not logged in, the
+   * promise is rejected and the page is redirected to the login flow.
+   *
+   * @param {!Object} data The parsed route data.
+   * @return {!Promise<!Object>} A promise yielding the original route data
+   *     (if it resolves).
+   */
+  _redirectIfNotLoggedIn(data) {
+    return this.$.restAPI.getLoggedIn().then(loggedIn => {
+      if (loggedIn) {
+        return Promise.resolve();
       } else {
-        throw new Error('Can\'t generate');
+        this._redirectToLogin(data.canonicalPath);
+        return Promise.reject(new Error());
       }
+    });
+  }
 
-      return base + url;
+  /**  Page.js middleware that warms the REST API's logged-in cache line. */
+  _loadUserMiddleware(ctx, next) {
+    this.$.restAPI.getLoggedIn().then(() => { next(); });
+  }
+
+  /**
+   * Map a route to a method on the router.
+   *
+   * @param {!string|!RegExp} pattern The page.js pattern for the route.
+   * @param {!string} handlerName The method name for the handler. If the
+   *     route is matched, the handler will be executed with `this` referring
+   *     to the component. Its return value will be discarded so that it does
+   *     not interfere with page.js.
+   * @param  {?boolean=} opt_authRedirect If true, then auth is checked before
+   *     executing the handler. If the user is not logged in, it will redirect
+   *     to the login flow and the handler will not be executed. The login
+   *     redirect specifies the matched URL to be used after successfull auth.
+   */
+  _mapRoute(pattern, handlerName, opt_authRedirect) {
+    if (!this[handlerName]) {
+      console.error('Attempted to map route to unknown method: ',
+          handlerName);
+      return;
+    }
+    page(pattern, this._loadUserMiddleware.bind(this), data => {
+      this.$.reporting.locationChanged(handlerName);
+      const promise = opt_authRedirect ?
+        this._redirectIfNotLoggedIn(data) : Promise.resolve();
+      promise.then(() => { this[handlerName](data); });
+    });
+  }
+
+  _startRouter() {
+    const base = this.getBaseUrl();
+    if (base) {
+      page.base(base);
     }
 
-    _generateWeblinks(params) {
-      const type = params.type;
-      switch (type) {
-        case Gerrit.Nav.WeblinkType.FILE:
-          return this._getFileWebLinks(params);
-        case Gerrit.Nav.WeblinkType.CHANGE:
-          return this._getChangeWeblinks(params);
-        case Gerrit.Nav.WeblinkType.PATCHSET:
-          return this._getPatchSetWeblink(params);
-        default:
-          console.warn(`Unsupported weblink ${type}!`);
+    Gerrit.Nav.setup(
+        url => { page.show(url); },
+        this._generateUrl.bind(this),
+        params => this._generateWeblinks(params),
+        x => x
+    );
+
+    page.exit('*', (ctx, next) => {
+      if (!this._isRedirecting) {
+        this.$.reporting.beforeLocationChanged();
       }
-    }
+      this._isRedirecting = false;
+      this._isInitialLoad = false;
+      next();
+    });
 
-    _getPatchSetWeblink(params) {
-      const {commit, options} = params;
-      const {weblinks, config} = options || {};
-      const name = commit && commit.slice(0, 7);
-      const weblink = this._getBrowseCommitWeblink(weblinks, config);
-      if (!weblink || !weblink.url) {
-        return {name};
-      } else {
-        return {name, url: weblink.url};
-      }
-    }
+    // Middleware
+    page((ctx, next) => {
+      document.body.scrollTop = 0;
 
-    _firstCodeBrowserWeblink(weblinks) {
-      // This is an ordered whitelist of web link types that provide direct
-      // links to the commit in the url property.
-      const codeBrowserLinks = ['gitiles', 'browse', 'gitweb'];
-      for (let i = 0; i < codeBrowserLinks.length; i++) {
-        const weblink =
-          weblinks.find(weblink => weblink.name === codeBrowserLinks[i]);
-        if (weblink) { return weblink; }
-      }
-      return null;
-    }
-
-    _getBrowseCommitWeblink(weblinks, config) {
-      if (!weblinks) { return null; }
-      let weblink;
-      // Use primary weblink if configured and exists.
-      if (config && config.gerrit && config.gerrit.primary_weblink_name) {
-        weblink = weblinks.find(
-            weblink => weblink.name === config.gerrit.primary_weblink_name
-        );
-      }
-      if (!weblink) {
-        weblink = this._firstCodeBrowserWeblink(weblinks);
-      }
-      if (!weblink) { return null; }
-      return weblink;
-    }
-
-    _getChangeWeblinks({repo, commit, options: {weblinks, config}}) {
-      if (!weblinks || !weblinks.length) return [];
-      const commitWeblink = this._getBrowseCommitWeblink(weblinks, config);
-      return weblinks.filter(weblink =>
-        !commitWeblink ||
-        !commitWeblink.name ||
-        weblink.name !== commitWeblink.name);
-    }
-
-    _getFileWebLinks({repo, commit, file, options: {weblinks}}) {
-      return weblinks;
-    }
-
-    /**
-     * @param {!Object} params
-     * @return {string}
-     */
-    _generateSearchUrl(params) {
-      let offsetExpr = '';
-      if (params.offset && params.offset > 0) {
-        offsetExpr = ',' + params.offset;
-      }
-
-      if (params.query) {
-        return '/q/' + this.encodeURL(params.query, true) + offsetExpr;
-      }
-
-      const operators = [];
-      if (params.owner) {
-        operators.push('owner:' + this.encodeURL(params.owner, false));
-      }
-      if (params.project) {
-        operators.push('project:' + this.encodeURL(params.project, false));
-      }
-      if (params.branch) {
-        operators.push('branch:' + this.encodeURL(params.branch, false));
-      }
-      if (params.topic) {
-        operators.push('topic:"' + this.encodeURL(params.topic, false) + '"');
-      }
-      if (params.hashtag) {
-        operators.push('hashtag:"' +
-            this.encodeURL(params.hashtag.toLowerCase(), false) + '"');
-      }
-      if (params.statuses) {
-        if (params.statuses.length === 1) {
-          operators.push(
-              'status:' + this.encodeURL(params.statuses[0], false));
-        } else if (params.statuses.length > 1) {
-          operators.push(
-              '(' +
-              params.statuses.map(s => `status:${this.encodeURL(s, false)}`)
-                  .join(' OR ') +
-              ')');
-        }
-      }
-
-      return '/q/' + operators.join('+') + offsetExpr;
-    }
-
-    /**
-     * @param {!Object} params
-     * @return {string}
-     */
-    _generateChangeUrl(params) {
-      let range = this._getPatchRangeExpression(params);
-      if (range.length) { range = '/' + range; }
-      let suffix = `${range}`;
-      if (params.querystring) {
-        suffix += '?' + params.querystring;
-      } else if (params.edit) {
-        suffix += ',edit';
-      }
-      if (params.messageHash) {
-        suffix += params.messageHash;
-      }
-      if (params.project) {
-        const encodedProject = this.encodeURL(params.project, true);
-        return `/c/${encodedProject}/+/${params.changeNum}${suffix}`;
-      } else {
-        return `/c/${params.changeNum}${suffix}`;
-      }
-    }
-
-    /**
-     * @param {!Object} params
-     * @return {string}
-     */
-    _generateDashboardUrl(params) {
-      const repoName = params.repo || params.project || null;
-      if (params.sections) {
-        // Custom dashboard.
-        const queryParams = this._sectionsToEncodedParams(params.sections,
-            repoName);
-        if (params.title) {
-          queryParams.push('title=' + encodeURIComponent(params.title));
-        }
-        const user = params.user ? params.user : '';
-        return `/dashboard/${user}?${queryParams.join('&')}`;
-      } else if (repoName) {
-        // Project dashboard.
-        const encodedRepo = this.encodeURL(repoName, true);
-        return `/p/${encodedRepo}/+/dashboard/${params.dashboard}`;
-      } else {
-        // User dashboard.
-        return `/dashboard/${params.user || 'self'}`;
-      }
-    }
-
-    /**
-     * @param {!Array<!{name: string, query: string}>} sections
-     * @param {string=} opt_repoName
-     * @return {!Array<string>}
-     */
-    _sectionsToEncodedParams(sections, opt_repoName) {
-      return sections.map(section => {
-        // If there is a repo name provided, make sure to substitute it into the
-        // ${repo} (or legacy ${project}) query tokens.
-        const query = opt_repoName ?
-          section.query.replace(REPO_TOKEN_PATTERN, opt_repoName) :
-          section.query;
-        return encodeURIComponent(section.name) + '=' +
-            encodeURIComponent(query);
-      });
-    }
-
-    /**
-     * @param {!Object} params
-     * @return {string}
-     */
-    _generateDiffOrEditUrl(params) {
-      let range = this._getPatchRangeExpression(params);
-      if (range.length) { range = '/' + range; }
-
-      let suffix = `${range}/${this.encodeURL(params.path, true)}`;
-
-      if (params.view === Gerrit.Nav.View.EDIT) { suffix += ',edit'; }
-
-      if (params.lineNum) {
-        suffix += '#';
-        if (params.leftSide) { suffix += 'b'; }
-        suffix += params.lineNum;
-      }
-
-      if (params.project) {
-        const encodedProject = this.encodeURL(params.project, true);
-        return `/c/${encodedProject}/+/${params.changeNum}${suffix}`;
-      } else {
-        return `/c/${params.changeNum}${suffix}`;
-      }
-    }
-
-    /**
-     * @param {!Object} params
-     * @return {string}
-     */
-    _generateGroupUrl(params) {
-      let url = `/admin/groups/${this.encodeURL(params.groupId + '', true)}`;
-      if (params.detail === Gerrit.Nav.GroupDetailView.MEMBERS) {
-        url += ',members';
-      } else if (params.detail === Gerrit.Nav.GroupDetailView.LOG) {
-        url += ',audit-log';
-      }
-      return url;
-    }
-
-    /**
-     * @param {!Object} params
-     * @return {string}
-     */
-    _generateRepoUrl(params) {
-      let url = `/admin/repos/${this.encodeURL(params.repoName + '', true)}`;
-      if (params.detail === Gerrit.Nav.RepoDetailView.ACCESS) {
-        url += ',access';
-      } else if (params.detail === Gerrit.Nav.RepoDetailView.BRANCHES) {
-        url += ',branches';
-      } else if (params.detail === Gerrit.Nav.RepoDetailView.TAGS) {
-        url += ',tags';
-      } else if (params.detail === Gerrit.Nav.RepoDetailView.COMMANDS) {
-        url += ',commands';
-      } else if (params.detail === Gerrit.Nav.RepoDetailView.DASHBOARDS) {
-        url += ',dashboards';
-      }
-      return url;
-    }
-
-    /**
-     * @param {!Object} params
-     * @return {string}
-     */
-    _generateSettingsUrl(params) {
-      return '/settings';
-    }
-
-    /**
-     * Given an object of parameters, potentially including a `patchNum` or a
-     * `basePatchNum` or both, return a string representation of that range. If
-     * no range is indicated in the params, the empty string is returned.
-     *
-     * @param {!Object} params
-     * @return {string}
-     */
-    _getPatchRangeExpression(params) {
-      let range = '';
-      if (params.patchNum) { range = '' + params.patchNum; }
-      if (params.basePatchNum) { range = params.basePatchNum + '..' + range; }
-      return range;
-    }
-
-    /**
-     * Given a set of params without a project, gets the project from the rest
-     * API project lookup and then sets the app params.
-     *
-     * @param {?Object} params
-     */
-    _normalizeLegacyRouteParams(params) {
-      if (!params.changeNum) { return Promise.resolve(); }
-
-      return this.$.restAPI.getFromProjectLookup(params.changeNum)
-          .then(project => {
-            // Show a 404 and terminate if the lookup request failed. Attempting
-            // to redirect after failing to get the project loops infinitely.
-            if (!project) {
-              this._show404();
-              return;
-            }
-
-            params.project = project;
-            this._normalizePatchRangeParams(params);
-            this._redirect(this._generateUrl(params));
-          });
-    }
-
-    /**
-     * Normalizes the params object, and determines if the URL needs to be
-     * modified to fit the proper schema.
-     *
-     * @param {*} params
-     * @return {boolean} whether or not the URL needs to be upgraded.
-     */
-    _normalizePatchRangeParams(params) {
-      const hasBasePatchNum = params.basePatchNum !== null &&
-          params.basePatchNum !== undefined;
-      const hasPatchNum = params.patchNum !== null &&
-          params.patchNum !== undefined;
-      let needsRedirect = false;
-
-      // Diffing a patch against itself is invalid, so if the base and revision
-      // patches are equal clear the base.
-      if (hasBasePatchNum &&
-          this.patchNumEquals(params.basePatchNum, params.patchNum)) {
-        needsRedirect = true;
-        params.basePatchNum = null;
-      } else if (hasBasePatchNum && !hasPatchNum) {
-        // Regexes set basePatchNum instead of patchNum when only one is
-        // specified. Redirect is not needed in this case.
-        params.patchNum = params.basePatchNum;
-        params.basePatchNum = null;
-      }
-      return needsRedirect;
-    }
-
-    /**
-     * Redirect the user to login using the given return-URL for redirection
-     * after authentication success.
-     *
-     * @param {string} returnUrl
-     */
-    _redirectToLogin(returnUrl) {
-      const basePath = this.getBaseUrl() || '';
-      page(
-          '/login/' + encodeURIComponent(returnUrl.substring(basePath.length)));
-    }
-
-    /**
-     * Hashes parsed by page.js exclude "inner" hashes, so a URL like "/a#b#c"
-     * is parsed to have a hash of "b" rather than "b#c". Instead, this method
-     * parses hashes correctly. Will return an empty string if there is no hash.
-     *
-     * @param {!string} canonicalPath
-     * @return {!string} Everything after the first '#' ("a#b#c" -> "b#c").
-     */
-    _getHashFromCanonicalPath(canonicalPath) {
-      return canonicalPath.split('#').slice(1)
-          .join('#');
-    }
-
-    _parseLineAddress(hash) {
-      const match = hash.match(LINE_ADDRESS_PATTERN);
-      if (!match) { return null; }
-      return {
-        leftSide: !!match[1],
-        lineNum: parseInt(match[2], 10),
-      };
-    }
-
-    /**
-     * Check to see if the user is logged in and return a promise that only
-     * resolves if the user is logged in. If the user us not logged in, the
-     * promise is rejected and the page is redirected to the login flow.
-     *
-     * @param {!Object} data The parsed route data.
-     * @return {!Promise<!Object>} A promise yielding the original route data
-     *     (if it resolves).
-     */
-    _redirectIfNotLoggedIn(data) {
-      return this.$.restAPI.getLoggedIn().then(loggedIn => {
-        if (loggedIn) {
-          return Promise.resolve();
-        } else {
-          this._redirectToLogin(data.canonicalPath);
-          return Promise.reject(new Error());
-        }
-      });
-    }
-
-    /**  Page.js middleware that warms the REST API's logged-in cache line. */
-    _loadUserMiddleware(ctx, next) {
-      this.$.restAPI.getLoggedIn().then(() => { next(); });
-    }
-
-    /**
-     * Map a route to a method on the router.
-     *
-     * @param {!string|!RegExp} pattern The page.js pattern for the route.
-     * @param {!string} handlerName The method name for the handler. If the
-     *     route is matched, the handler will be executed with `this` referring
-     *     to the component. Its return value will be discarded so that it does
-     *     not interfere with page.js.
-     * @param  {?boolean=} opt_authRedirect If true, then auth is checked before
-     *     executing the handler. If the user is not logged in, it will redirect
-     *     to the login flow and the handler will not be executed. The login
-     *     redirect specifies the matched URL to be used after successfull auth.
-     */
-    _mapRoute(pattern, handlerName, opt_authRedirect) {
-      if (!this[handlerName]) {
-        console.error('Attempted to map route to unknown method: ',
-            handlerName);
+      if (ctx.hash.match(RoutePattern.PLUGIN_SCREEN)) {
+        // Redirect all urls using hash #/x/plugin/screen to /x/plugin/screen
+        // This is needed to allow plugins to add basic #/x/ screen links to
+        // any location.
+        this._redirect(ctx.hash);
         return;
       }
-      page(pattern, this._loadUserMiddleware.bind(this), data => {
-        this.$.reporting.locationChanged(handlerName);
-        const promise = opt_authRedirect ?
-          this._redirectIfNotLoggedIn(data) : Promise.resolve();
-        promise.then(() => { this[handlerName](data); });
-      });
-    }
 
-    _startRouter() {
+      // Fire asynchronously so that the URL is changed by the time the event
+      // is processed.
+      this.async(() => {
+        this.fire('location-change', {
+          hash: window.location.hash,
+          pathname: window.location.pathname,
+        });
+      }, 1);
+      next();
+    });
+
+    this._mapRoute(RoutePattern.ROOT, '_handleRootRoute');
+
+    this._mapRoute(RoutePattern.DASHBOARD, '_handleDashboardRoute');
+
+    this._mapRoute(RoutePattern.CUSTOM_DASHBOARD,
+        '_handleCustomDashboardRoute');
+
+    this._mapRoute(RoutePattern.PROJECT_DASHBOARD,
+        '_handleProjectDashboardRoute');
+
+    this._mapRoute(RoutePattern.GROUP_INFO, '_handleGroupInfoRoute', true);
+
+    this._mapRoute(RoutePattern.GROUP_AUDIT_LOG, '_handleGroupAuditLogRoute',
+        true);
+
+    this._mapRoute(RoutePattern.GROUP_MEMBERS, '_handleGroupMembersRoute',
+        true);
+
+    this._mapRoute(RoutePattern.GROUP_LIST_OFFSET,
+        '_handleGroupListOffsetRoute', true);
+
+    this._mapRoute(RoutePattern.GROUP_LIST_FILTER_OFFSET,
+        '_handleGroupListFilterOffsetRoute', true);
+
+    this._mapRoute(RoutePattern.GROUP_LIST_FILTER,
+        '_handleGroupListFilterRoute', true);
+
+    this._mapRoute(RoutePattern.GROUP_SELF, '_handleGroupSelfRedirectRoute',
+        true);
+
+    this._mapRoute(RoutePattern.GROUP, '_handleGroupRoute', true);
+
+    this._mapRoute(RoutePattern.PROJECT_OLD,
+        '_handleProjectsOldRoute');
+
+    this._mapRoute(RoutePattern.REPO_COMMANDS,
+        '_handleRepoCommandsRoute', true);
+
+    this._mapRoute(RoutePattern.REPO_ACCESS,
+        '_handleRepoAccessRoute');
+
+    this._mapRoute(RoutePattern.REPO_DASHBOARDS,
+        '_handleRepoDashboardsRoute');
+
+    this._mapRoute(RoutePattern.BRANCH_LIST_OFFSET,
+        '_handleBranchListOffsetRoute');
+
+    this._mapRoute(RoutePattern.BRANCH_LIST_FILTER_OFFSET,
+        '_handleBranchListFilterOffsetRoute');
+
+    this._mapRoute(RoutePattern.BRANCH_LIST_FILTER,
+        '_handleBranchListFilterRoute');
+
+    this._mapRoute(RoutePattern.TAG_LIST_OFFSET,
+        '_handleTagListOffsetRoute');
+
+    this._mapRoute(RoutePattern.TAG_LIST_FILTER_OFFSET,
+        '_handleTagListFilterOffsetRoute');
+
+    this._mapRoute(RoutePattern.TAG_LIST_FILTER,
+        '_handleTagListFilterRoute');
+
+    this._mapRoute(RoutePattern.LEGACY_CREATE_GROUP,
+        '_handleCreateGroupRoute', true);
+
+    this._mapRoute(RoutePattern.LEGACY_CREATE_PROJECT,
+        '_handleCreateProjectRoute', true);
+
+    this._mapRoute(RoutePattern.REPO_LIST_OFFSET,
+        '_handleRepoListOffsetRoute');
+
+    this._mapRoute(RoutePattern.REPO_LIST_FILTER_OFFSET,
+        '_handleRepoListFilterOffsetRoute');
+
+    this._mapRoute(RoutePattern.REPO_LIST_FILTER,
+        '_handleRepoListFilterRoute');
+
+    this._mapRoute(RoutePattern.REPO, '_handleRepoRoute');
+
+    this._mapRoute(RoutePattern.PLUGINS, '_handlePassThroughRoute');
+
+    this._mapRoute(RoutePattern.PLUGIN_LIST_OFFSET,
+        '_handlePluginListOffsetRoute', true);
+
+    this._mapRoute(RoutePattern.PLUGIN_LIST_FILTER_OFFSET,
+        '_handlePluginListFilterOffsetRoute', true);
+
+    this._mapRoute(RoutePattern.PLUGIN_LIST_FILTER,
+        '_handlePluginListFilterRoute', true);
+
+    this._mapRoute(RoutePattern.PLUGIN_LIST, '_handlePluginListRoute', true);
+
+    this._mapRoute(RoutePattern.QUERY_LEGACY_SUFFIX,
+        '_handleQueryLegacySuffixRoute');
+
+    this._mapRoute(RoutePattern.QUERY, '_handleQueryRoute');
+
+    this._mapRoute(RoutePattern.DIFF_LEGACY_LINENUM, '_handleLegacyLinenum');
+
+    this._mapRoute(RoutePattern.CHANGE_NUMBER_LEGACY,
+        '_handleChangeNumberLegacyRoute');
+
+    this._mapRoute(RoutePattern.DIFF_EDIT, '_handleDiffEditRoute', true);
+
+    this._mapRoute(RoutePattern.CHANGE_EDIT, '_handleChangeEditRoute', true);
+
+    this._mapRoute(RoutePattern.DIFF, '_handleDiffRoute');
+
+    this._mapRoute(RoutePattern.CHANGE, '_handleChangeRoute');
+
+    this._mapRoute(RoutePattern.CHANGE_LEGACY, '_handleChangeLegacyRoute');
+
+    this._mapRoute(RoutePattern.DIFF_LEGACY, '_handleDiffLegacyRoute');
+
+    this._mapRoute(RoutePattern.AGREEMENTS, '_handleAgreementsRoute', true);
+
+    this._mapRoute(RoutePattern.NEW_AGREEMENTS, '_handleNewAgreementsRoute',
+        true);
+
+    this._mapRoute(RoutePattern.SETTINGS_LEGACY,
+        '_handleSettingsLegacyRoute', true);
+
+    this._mapRoute(RoutePattern.SETTINGS, '_handleSettingsRoute', true);
+
+    this._mapRoute(RoutePattern.REGISTER, '_handleRegisterRoute');
+
+    this._mapRoute(RoutePattern.LOG_IN_OR_OUT, '_handlePassThroughRoute');
+
+    this._mapRoute(RoutePattern.IMPROPERLY_ENCODED_PLUS,
+        '_handleImproperlyEncodedPlusRoute');
+
+    this._mapRoute(RoutePattern.PLUGIN_SCREEN, '_handlePluginScreen');
+
+    this._mapRoute(RoutePattern.DOCUMENTATION_SEARCH_FILTER,
+        '_handleDocumentationSearchRoute');
+
+    // redirects /Documentation/q/* to /Documentation/q/filter:*
+    this._mapRoute(RoutePattern.DOCUMENTATION_SEARCH,
+        '_handleDocumentationSearchRedirectRoute');
+
+    // Makes sure /Documentation/* links work (doin't return 404)
+    this._mapRoute(RoutePattern.DOCUMENTATION,
+        '_handleDocumentationRedirectRoute');
+
+    // Note: this route should appear last so it only catches URLs unmatched
+    // by other patterns.
+    this._mapRoute(RoutePattern.DEFAULT, '_handleDefaultRoute');
+
+    page.start();
+  }
+
+  /**
+   * @param {!Object} data
+   * @return {Promise|null} if handling the route involves asynchrony, then a
+   *     promise is returned. Otherwise, synchronous handling returns null.
+   */
+  _handleRootRoute(data) {
+    if (data.querystring.match(/^closeAfterLogin/)) {
+      // Close child window on redirect after login.
+      window.close();
+      return null;
+    }
+    let hash = this._getHashFromCanonicalPath(data.canonicalPath);
+    // For backward compatibility with GWT links.
+    if (hash) {
+      // In certain login flows the server may redirect to a hash without
+      // a leading slash, which page.js doesn't handle correctly.
+      if (hash[0] !== '/') {
+        hash = '/' + hash;
+      }
+      if (hash.includes('/ /') && data.canonicalPath.includes('/+/')) {
+        // Path decodes all '+' to ' ' -- this breaks project-based URLs.
+        // See Issue 6888.
+        hash = hash.replace('/ /', '/+/');
+      }
       const base = this.getBaseUrl();
-      if (base) {
-        page.base(base);
+      let newUrl = base + hash;
+      if (hash.startsWith('/VE/')) {
+        newUrl = base + '/settings' + hash;
       }
-
-      Gerrit.Nav.setup(
-          url => { page.show(url); },
-          this._generateUrl.bind(this),
-          params => this._generateWeblinks(params),
-          x => x
-      );
-
-      page.exit('*', (ctx, next) => {
-        if (!this._isRedirecting) {
-          this.$.reporting.beforeLocationChanged();
-        }
-        this._isRedirecting = false;
-        this._isInitialLoad = false;
-        next();
-      });
-
-      // Middleware
-      page((ctx, next) => {
-        document.body.scrollTop = 0;
-
-        if (ctx.hash.match(RoutePattern.PLUGIN_SCREEN)) {
-          // Redirect all urls using hash #/x/plugin/screen to /x/plugin/screen
-          // This is needed to allow plugins to add basic #/x/ screen links to
-          // any location.
-          this._redirect(ctx.hash);
-          return;
-        }
-
-        // Fire asynchronously so that the URL is changed by the time the event
-        // is processed.
-        this.async(() => {
-          this.fire('location-change', {
-            hash: window.location.hash,
-            pathname: window.location.pathname,
-          });
-        }, 1);
-        next();
-      });
-
-      this._mapRoute(RoutePattern.ROOT, '_handleRootRoute');
-
-      this._mapRoute(RoutePattern.DASHBOARD, '_handleDashboardRoute');
-
-      this._mapRoute(RoutePattern.CUSTOM_DASHBOARD,
-          '_handleCustomDashboardRoute');
-
-      this._mapRoute(RoutePattern.PROJECT_DASHBOARD,
-          '_handleProjectDashboardRoute');
-
-      this._mapRoute(RoutePattern.GROUP_INFO, '_handleGroupInfoRoute', true);
-
-      this._mapRoute(RoutePattern.GROUP_AUDIT_LOG, '_handleGroupAuditLogRoute',
-          true);
-
-      this._mapRoute(RoutePattern.GROUP_MEMBERS, '_handleGroupMembersRoute',
-          true);
-
-      this._mapRoute(RoutePattern.GROUP_LIST_OFFSET,
-          '_handleGroupListOffsetRoute', true);
-
-      this._mapRoute(RoutePattern.GROUP_LIST_FILTER_OFFSET,
-          '_handleGroupListFilterOffsetRoute', true);
-
-      this._mapRoute(RoutePattern.GROUP_LIST_FILTER,
-          '_handleGroupListFilterRoute', true);
-
-      this._mapRoute(RoutePattern.GROUP_SELF, '_handleGroupSelfRedirectRoute',
-          true);
-
-      this._mapRoute(RoutePattern.GROUP, '_handleGroupRoute', true);
-
-      this._mapRoute(RoutePattern.PROJECT_OLD,
-          '_handleProjectsOldRoute');
-
-      this._mapRoute(RoutePattern.REPO_COMMANDS,
-          '_handleRepoCommandsRoute', true);
-
-      this._mapRoute(RoutePattern.REPO_ACCESS,
-          '_handleRepoAccessRoute');
-
-      this._mapRoute(RoutePattern.REPO_DASHBOARDS,
-          '_handleRepoDashboardsRoute');
-
-      this._mapRoute(RoutePattern.BRANCH_LIST_OFFSET,
-          '_handleBranchListOffsetRoute');
-
-      this._mapRoute(RoutePattern.BRANCH_LIST_FILTER_OFFSET,
-          '_handleBranchListFilterOffsetRoute');
-
-      this._mapRoute(RoutePattern.BRANCH_LIST_FILTER,
-          '_handleBranchListFilterRoute');
-
-      this._mapRoute(RoutePattern.TAG_LIST_OFFSET,
-          '_handleTagListOffsetRoute');
-
-      this._mapRoute(RoutePattern.TAG_LIST_FILTER_OFFSET,
-          '_handleTagListFilterOffsetRoute');
-
-      this._mapRoute(RoutePattern.TAG_LIST_FILTER,
-          '_handleTagListFilterRoute');
-
-      this._mapRoute(RoutePattern.LEGACY_CREATE_GROUP,
-          '_handleCreateGroupRoute', true);
-
-      this._mapRoute(RoutePattern.LEGACY_CREATE_PROJECT,
-          '_handleCreateProjectRoute', true);
-
-      this._mapRoute(RoutePattern.REPO_LIST_OFFSET,
-          '_handleRepoListOffsetRoute');
-
-      this._mapRoute(RoutePattern.REPO_LIST_FILTER_OFFSET,
-          '_handleRepoListFilterOffsetRoute');
-
-      this._mapRoute(RoutePattern.REPO_LIST_FILTER,
-          '_handleRepoListFilterRoute');
-
-      this._mapRoute(RoutePattern.REPO, '_handleRepoRoute');
-
-      this._mapRoute(RoutePattern.PLUGINS, '_handlePassThroughRoute');
-
-      this._mapRoute(RoutePattern.PLUGIN_LIST_OFFSET,
-          '_handlePluginListOffsetRoute', true);
-
-      this._mapRoute(RoutePattern.PLUGIN_LIST_FILTER_OFFSET,
-          '_handlePluginListFilterOffsetRoute', true);
-
-      this._mapRoute(RoutePattern.PLUGIN_LIST_FILTER,
-          '_handlePluginListFilterRoute', true);
-
-      this._mapRoute(RoutePattern.PLUGIN_LIST, '_handlePluginListRoute', true);
-
-      this._mapRoute(RoutePattern.QUERY_LEGACY_SUFFIX,
-          '_handleQueryLegacySuffixRoute');
-
-      this._mapRoute(RoutePattern.QUERY, '_handleQueryRoute');
-
-      this._mapRoute(RoutePattern.DIFF_LEGACY_LINENUM, '_handleLegacyLinenum');
-
-      this._mapRoute(RoutePattern.CHANGE_NUMBER_LEGACY,
-          '_handleChangeNumberLegacyRoute');
-
-      this._mapRoute(RoutePattern.DIFF_EDIT, '_handleDiffEditRoute', true);
-
-      this._mapRoute(RoutePattern.CHANGE_EDIT, '_handleChangeEditRoute', true);
-
-      this._mapRoute(RoutePattern.DIFF, '_handleDiffRoute');
-
-      this._mapRoute(RoutePattern.CHANGE, '_handleChangeRoute');
-
-      this._mapRoute(RoutePattern.CHANGE_LEGACY, '_handleChangeLegacyRoute');
-
-      this._mapRoute(RoutePattern.DIFF_LEGACY, '_handleDiffLegacyRoute');
-
-      this._mapRoute(RoutePattern.AGREEMENTS, '_handleAgreementsRoute', true);
-
-      this._mapRoute(RoutePattern.NEW_AGREEMENTS, '_handleNewAgreementsRoute',
-          true);
-
-      this._mapRoute(RoutePattern.SETTINGS_LEGACY,
-          '_handleSettingsLegacyRoute', true);
-
-      this._mapRoute(RoutePattern.SETTINGS, '_handleSettingsRoute', true);
-
-      this._mapRoute(RoutePattern.REGISTER, '_handleRegisterRoute');
-
-      this._mapRoute(RoutePattern.LOG_IN_OR_OUT, '_handlePassThroughRoute');
-
-      this._mapRoute(RoutePattern.IMPROPERLY_ENCODED_PLUS,
-          '_handleImproperlyEncodedPlusRoute');
-
-      this._mapRoute(RoutePattern.PLUGIN_SCREEN, '_handlePluginScreen');
-
-      this._mapRoute(RoutePattern.DOCUMENTATION_SEARCH_FILTER,
-          '_handleDocumentationSearchRoute');
-
-      // redirects /Documentation/q/* to /Documentation/q/filter:*
-      this._mapRoute(RoutePattern.DOCUMENTATION_SEARCH,
-          '_handleDocumentationSearchRedirectRoute');
-
-      // Makes sure /Documentation/* links work (doin't return 404)
-      this._mapRoute(RoutePattern.DOCUMENTATION,
-          '_handleDocumentationRedirectRoute');
-
-      // Note: this route should appear last so it only catches URLs unmatched
-      // by other patterns.
-      this._mapRoute(RoutePattern.DEFAULT, '_handleDefaultRoute');
-
-      page.start();
+      this._redirect(newUrl);
+      return null;
     }
+    return this.$.restAPI.getLoggedIn().then(loggedIn => {
+      if (loggedIn) {
+        this._redirect('/dashboard/self');
+      } else {
+        this._redirect('/q/status:open');
+      }
+    });
+  }
 
-    /**
-     * @param {!Object} data
-     * @return {Promise|null} if handling the route involves asynchrony, then a
-     *     promise is returned. Otherwise, synchronous handling returns null.
-     */
-    _handleRootRoute(data) {
-      if (data.querystring.match(/^closeAfterLogin/)) {
-        // Close child window on redirect after login.
-        window.close();
-        return null;
+  /**
+   * Decode an application/x-www-form-urlencoded string.
+   *
+   * @param {string} qs The application/x-www-form-urlencoded string.
+   * @return {string} The decoded string.
+   */
+  _decodeQueryString(qs) {
+    return decodeURIComponent(qs.replace(PLUS_PATTERN, ' '));
+  }
+
+  /**
+   * Parse a query string (e.g. window.location.search) into an array of
+   * name/value pairs.
+   *
+   * @param {string} qs The application/x-www-form-urlencoded query string.
+   * @return {!Array<!Array<string>>} An array of name/value pairs, where each
+   *     element is a 2-element array.
+   */
+  _parseQueryString(qs) {
+    qs = qs.replace(QUESTION_PATTERN, '');
+    if (!qs) {
+      return [];
+    }
+    const params = [];
+    qs.split('&').forEach(param => {
+      const idx = param.indexOf('=');
+      let name;
+      let value;
+      if (idx < 0) {
+        name = this._decodeQueryString(param);
+        value = '';
+      } else {
+        name = this._decodeQueryString(param.substring(0, idx));
+        value = this._decodeQueryString(param.substring(idx + 1));
       }
-      let hash = this._getHashFromCanonicalPath(data.canonicalPath);
-      // For backward compatibility with GWT links.
-      if (hash) {
-        // In certain login flows the server may redirect to a hash without
-        // a leading slash, which page.js doesn't handle correctly.
-        if (hash[0] !== '/') {
-          hash = '/' + hash;
-        }
-        if (hash.includes('/ /') && data.canonicalPath.includes('/+/')) {
-          // Path decodes all '+' to ' ' -- this breaks project-based URLs.
-          // See Issue 6888.
-          hash = hash.replace('/ /', '/+/');
-        }
-        const base = this.getBaseUrl();
-        let newUrl = base + hash;
-        if (hash.startsWith('/VE/')) {
-          newUrl = base + '/settings' + hash;
-        }
-        this._redirect(newUrl);
-        return null;
+      if (name) {
+        params.push([name, value]);
       }
-      return this.$.restAPI.getLoggedIn().then(loggedIn => {
-        if (loggedIn) {
-          this._redirect('/dashboard/self');
+    });
+    return params;
+  }
+
+  /**
+   * Handle dashboard routes. These may be user, or project dashboards.
+   *
+   * @param {!Object} data The parsed route data.
+   */
+  _handleDashboardRoute(data) {
+    // User dashboard. We require viewing user to be logged in, else we
+    // redirect to login for self dashboard or simple owner search for
+    // other user dashboard.
+    return this.$.restAPI.getLoggedIn().then(loggedIn => {
+      if (!loggedIn) {
+        if (data.params[0].toLowerCase() === 'self') {
+          this._redirectToLogin(data.canonicalPath);
         } else {
-          this._redirect('/q/status:open');
+          this._redirect('/q/owner:' + encodeURIComponent(data.params[0]));
         }
-      });
-    }
-
-    /**
-     * Decode an application/x-www-form-urlencoded string.
-     *
-     * @param {string} qs The application/x-www-form-urlencoded string.
-     * @return {string} The decoded string.
-     */
-    _decodeQueryString(qs) {
-      return decodeURIComponent(qs.replace(PLUS_PATTERN, ' '));
-    }
-
-    /**
-     * Parse a query string (e.g. window.location.search) into an array of
-     * name/value pairs.
-     *
-     * @param {string} qs The application/x-www-form-urlencoded query string.
-     * @return {!Array<!Array<string>>} An array of name/value pairs, where each
-     *     element is a 2-element array.
-     */
-    _parseQueryString(qs) {
-      qs = qs.replace(QUESTION_PATTERN, '');
-      if (!qs) {
-        return [];
-      }
-      const params = [];
-      qs.split('&').forEach(param => {
-        const idx = param.indexOf('=');
-        let name;
-        let value;
-        if (idx < 0) {
-          name = this._decodeQueryString(param);
-          value = '';
-        } else {
-          name = this._decodeQueryString(param.substring(0, idx));
-          value = this._decodeQueryString(param.substring(idx + 1));
-        }
-        if (name) {
-          params.push([name, value]);
-        }
-      });
-      return params;
-    }
-
-    /**
-     * Handle dashboard routes. These may be user, or project dashboards.
-     *
-     * @param {!Object} data The parsed route data.
-     */
-    _handleDashboardRoute(data) {
-      // User dashboard. We require viewing user to be logged in, else we
-      // redirect to login for self dashboard or simple owner search for
-      // other user dashboard.
-      return this.$.restAPI.getLoggedIn().then(loggedIn => {
-        if (!loggedIn) {
-          if (data.params[0].toLowerCase() === 'self') {
-            this._redirectToLogin(data.canonicalPath);
-          } else {
-            this._redirect('/q/owner:' + encodeURIComponent(data.params[0]));
-          }
-        } else {
-          this._setParams({
-            view: Gerrit.Nav.View.DASHBOARD,
-            user: data.params[0],
-          });
-        }
-      });
-    }
-
-    /**
-     * Handle custom dashboard routes.
-     *
-     * @param {!Object} data The parsed route data.
-     * @param {string=} opt_qs Optional query string associated with the route.
-     *     If not given, window.location.search is used. (Used by tests).
-     */
-    _handleCustomDashboardRoute(data, opt_qs) {
-      // opt_qs may be provided by a test, and it may have a falsy value
-      const qs = opt_qs !== undefined ? opt_qs : window.location.search;
-      const queryParams = this._parseQueryString(qs);
-      let title = 'Custom Dashboard';
-      const titleParam = queryParams.find(
-          elem => elem[0].toLowerCase() === 'title');
-      if (titleParam) {
-        title = titleParam[1];
-      }
-      // Dashboards support a foreach param which adds a base query to any
-      // additional query.
-      const forEachParam = queryParams.find(
-          elem => elem[0].toLowerCase() === 'foreach');
-      let forEachQuery = null;
-      if (forEachParam) {
-        forEachQuery = forEachParam[1];
-      }
-      const sectionParams = queryParams.filter(
-          elem => elem[0] && elem[1] && elem[0].toLowerCase() !== 'title' &&
-          elem[0].toLowerCase() !== 'foreach');
-      const sections = sectionParams.map(elem => {
-        const query = forEachQuery ? `${forEachQuery} ${elem[1]}` : elem[1];
-        return {
-          name: elem[0],
-          query,
-        };
-      });
-
-      if (sections.length > 0) {
-        // Custom dashboard view.
+      } else {
         this._setParams({
           view: Gerrit.Nav.View.DASHBOARD,
-          user: 'self',
-          sections,
-          title,
+          user: data.params[0],
         });
-        return Promise.resolve();
       }
+    });
+  }
 
-      // Redirect /dashboard/ -> /dashboard/self.
-      this._redirect('/dashboard/self');
+  /**
+   * Handle custom dashboard routes.
+   *
+   * @param {!Object} data The parsed route data.
+   * @param {string=} opt_qs Optional query string associated with the route.
+   *     If not given, window.location.search is used. (Used by tests).
+   */
+  _handleCustomDashboardRoute(data, opt_qs) {
+    // opt_qs may be provided by a test, and it may have a falsy value
+    const qs = opt_qs !== undefined ? opt_qs : window.location.search;
+    const queryParams = this._parseQueryString(qs);
+    let title = 'Custom Dashboard';
+    const titleParam = queryParams.find(
+        elem => elem[0].toLowerCase() === 'title');
+    if (titleParam) {
+      title = titleParam[1];
+    }
+    // Dashboards support a foreach param which adds a base query to any
+    // additional query.
+    const forEachParam = queryParams.find(
+        elem => elem[0].toLowerCase() === 'foreach');
+    let forEachQuery = null;
+    if (forEachParam) {
+      forEachQuery = forEachParam[1];
+    }
+    const sectionParams = queryParams.filter(
+        elem => elem[0] && elem[1] && elem[0].toLowerCase() !== 'title' &&
+        elem[0].toLowerCase() !== 'foreach');
+    const sections = sectionParams.map(elem => {
+      const query = forEachQuery ? `${forEachQuery} ${elem[1]}` : elem[1];
+      return {
+        name: elem[0],
+        query,
+      };
+    });
+
+    if (sections.length > 0) {
+      // Custom dashboard view.
+      this._setParams({
+        view: Gerrit.Nav.View.DASHBOARD,
+        user: 'self',
+        sections,
+        title,
+      });
       return Promise.resolve();
     }
 
-    _handleProjectDashboardRoute(data) {
-      const project = data.params[0];
-      this._setParams({
-        view: Gerrit.Nav.View.DASHBOARD,
-        project,
-        dashboard: decodeURIComponent(data.params[1]),
-      });
-      this.$.reporting.setRepoName(project);
-    }
+    // Redirect /dashboard/ -> /dashboard/self.
+    this._redirect('/dashboard/self');
+    return Promise.resolve();
+  }
 
-    _handleGroupInfoRoute(data) {
-      this._redirect('/admin/groups/' + encodeURIComponent(data.params[0]));
-    }
+  _handleProjectDashboardRoute(data) {
+    const project = data.params[0];
+    this._setParams({
+      view: Gerrit.Nav.View.DASHBOARD,
+      project,
+      dashboard: decodeURIComponent(data.params[1]),
+    });
+    this.$.reporting.setRepoName(project);
+  }
 
-    _handleGroupSelfRedirectRoute(data) {
-      this._redirect('/settings/#Groups');
-    }
+  _handleGroupInfoRoute(data) {
+    this._redirect('/admin/groups/' + encodeURIComponent(data.params[0]));
+  }
 
-    _handleGroupRoute(data) {
-      this._setParams({
-        view: Gerrit.Nav.View.GROUP,
-        groupId: data.params[0],
-      });
-    }
+  _handleGroupSelfRedirectRoute(data) {
+    this._redirect('/settings/#Groups');
+  }
 
-    _handleGroupAuditLogRoute(data) {
-      this._setParams({
-        view: Gerrit.Nav.View.GROUP,
-        detail: Gerrit.Nav.GroupDetailView.LOG,
-        groupId: data.params[0],
-      });
-    }
+  _handleGroupRoute(data) {
+    this._setParams({
+      view: Gerrit.Nav.View.GROUP,
+      groupId: data.params[0],
+    });
+  }
 
-    _handleGroupMembersRoute(data) {
-      this._setParams({
-        view: Gerrit.Nav.View.GROUP,
-        detail: Gerrit.Nav.GroupDetailView.MEMBERS,
-        groupId: data.params[0],
-      });
-    }
+  _handleGroupAuditLogRoute(data) {
+    this._setParams({
+      view: Gerrit.Nav.View.GROUP,
+      detail: Gerrit.Nav.GroupDetailView.LOG,
+      groupId: data.params[0],
+    });
+  }
 
-    _handleGroupListOffsetRoute(data) {
-      this._setParams({
-        view: Gerrit.Nav.View.ADMIN,
-        adminView: 'gr-admin-group-list',
-        offset: data.params[1] || 0,
-        filter: null,
-        openCreateModal: data.hash === 'create',
-      });
-    }
+  _handleGroupMembersRoute(data) {
+    this._setParams({
+      view: Gerrit.Nav.View.GROUP,
+      detail: Gerrit.Nav.GroupDetailView.MEMBERS,
+      groupId: data.params[0],
+    });
+  }
 
-    _handleGroupListFilterOffsetRoute(data) {
-      this._setParams({
-        view: Gerrit.Nav.View.ADMIN,
-        adminView: 'gr-admin-group-list',
-        offset: data.params.offset,
-        filter: data.params.filter,
-      });
-    }
+  _handleGroupListOffsetRoute(data) {
+    this._setParams({
+      view: Gerrit.Nav.View.ADMIN,
+      adminView: 'gr-admin-group-list',
+      offset: data.params[1] || 0,
+      filter: null,
+      openCreateModal: data.hash === 'create',
+    });
+  }
 
-    _handleGroupListFilterRoute(data) {
-      this._setParams({
-        view: Gerrit.Nav.View.ADMIN,
-        adminView: 'gr-admin-group-list',
-        filter: data.params.filter || null,
-      });
-    }
+  _handleGroupListFilterOffsetRoute(data) {
+    this._setParams({
+      view: Gerrit.Nav.View.ADMIN,
+      adminView: 'gr-admin-group-list',
+      offset: data.params.offset,
+      filter: data.params.filter,
+    });
+  }
 
-    _handleProjectsOldRoute(data) {
-      let params = '';
-      if (data.params[1]) {
-        params = encodeURIComponent(data.params[1]);
-        if (data.params[1].includes(',')) {
-          params =
-              encodeURIComponent(data.params[1]).replace('%2C', ',');
-        }
-      }
+  _handleGroupListFilterRoute(data) {
+    this._setParams({
+      view: Gerrit.Nav.View.ADMIN,
+      adminView: 'gr-admin-group-list',
+      filter: data.params.filter || null,
+    });
+  }
 
-      this._redirect(`/admin/repos/${params}`);
-    }
-
-    _handleRepoCommandsRoute(data) {
-      const repo = data.params[0];
-      this._setParams({
-        view: Gerrit.Nav.View.REPO,
-        detail: Gerrit.Nav.RepoDetailView.COMMANDS,
-        repo,
-      });
-      this.$.reporting.setRepoName(repo);
-    }
-
-    _handleRepoAccessRoute(data) {
-      const repo = data.params[0];
-      this._setParams({
-        view: Gerrit.Nav.View.REPO,
-        detail: Gerrit.Nav.RepoDetailView.ACCESS,
-        repo,
-      });
-      this.$.reporting.setRepoName(repo);
-    }
-
-    _handleRepoDashboardsRoute(data) {
-      const repo = data.params[0];
-      this._setParams({
-        view: Gerrit.Nav.View.REPO,
-        detail: Gerrit.Nav.RepoDetailView.DASHBOARDS,
-        repo,
-      });
-      this.$.reporting.setRepoName(repo);
-    }
-
-    _handleBranchListOffsetRoute(data) {
-      this._setParams({
-        view: Gerrit.Nav.View.REPO,
-        detail: Gerrit.Nav.RepoDetailView.BRANCHES,
-        repo: data.params[0],
-        offset: data.params[2] || 0,
-        filter: null,
-      });
-    }
-
-    _handleBranchListFilterOffsetRoute(data) {
-      this._setParams({
-        view: Gerrit.Nav.View.REPO,
-        detail: Gerrit.Nav.RepoDetailView.BRANCHES,
-        repo: data.params.repo,
-        offset: data.params.offset,
-        filter: data.params.filter,
-      });
-    }
-
-    _handleBranchListFilterRoute(data) {
-      this._setParams({
-        view: Gerrit.Nav.View.REPO,
-        detail: Gerrit.Nav.RepoDetailView.BRANCHES,
-        repo: data.params.repo,
-        filter: data.params.filter || null,
-      });
-    }
-
-    _handleTagListOffsetRoute(data) {
-      this._setParams({
-        view: Gerrit.Nav.View.REPO,
-        detail: Gerrit.Nav.RepoDetailView.TAGS,
-        repo: data.params[0],
-        offset: data.params[2] || 0,
-        filter: null,
-      });
-    }
-
-    _handleTagListFilterOffsetRoute(data) {
-      this._setParams({
-        view: Gerrit.Nav.View.REPO,
-        detail: Gerrit.Nav.RepoDetailView.TAGS,
-        repo: data.params.repo,
-        offset: data.params.offset,
-        filter: data.params.filter,
-      });
-    }
-
-    _handleTagListFilterRoute(data) {
-      this._setParams({
-        view: Gerrit.Nav.View.REPO,
-        detail: Gerrit.Nav.RepoDetailView.TAGS,
-        repo: data.params.repo,
-        filter: data.params.filter || null,
-      });
-    }
-
-    _handleRepoListOffsetRoute(data) {
-      this._setParams({
-        view: Gerrit.Nav.View.ADMIN,
-        adminView: 'gr-repo-list',
-        offset: data.params[1] || 0,
-        filter: null,
-        openCreateModal: data.hash === 'create',
-      });
-    }
-
-    _handleRepoListFilterOffsetRoute(data) {
-      this._setParams({
-        view: Gerrit.Nav.View.ADMIN,
-        adminView: 'gr-repo-list',
-        offset: data.params.offset,
-        filter: data.params.filter,
-      });
-    }
-
-    _handleRepoListFilterRoute(data) {
-      this._setParams({
-        view: Gerrit.Nav.View.ADMIN,
-        adminView: 'gr-repo-list',
-        filter: data.params.filter || null,
-      });
-    }
-
-    _handleCreateProjectRoute(data) {
-      // Redirects the legacy route to the new route, which displays the project
-      // list with a hash 'create'.
-      this._redirect('/admin/repos#create');
-    }
-
-    _handleCreateGroupRoute(data) {
-      // Redirects the legacy route to the new route, which displays the group
-      // list with a hash 'create'.
-      this._redirect('/admin/groups#create');
-    }
-
-    _handleRepoRoute(data) {
-      const repo = data.params[0];
-      this._setParams({
-        view: Gerrit.Nav.View.REPO,
-        repo,
-      });
-      this.$.reporting.setRepoName(repo);
-    }
-
-    _handlePluginListOffsetRoute(data) {
-      this._setParams({
-        view: Gerrit.Nav.View.ADMIN,
-        adminView: 'gr-plugin-list',
-        offset: data.params[1] || 0,
-        filter: null,
-      });
-    }
-
-    _handlePluginListFilterOffsetRoute(data) {
-      this._setParams({
-        view: Gerrit.Nav.View.ADMIN,
-        adminView: 'gr-plugin-list',
-        offset: data.params.offset,
-        filter: data.params.filter,
-      });
-    }
-
-    _handlePluginListFilterRoute(data) {
-      this._setParams({
-        view: Gerrit.Nav.View.ADMIN,
-        adminView: 'gr-plugin-list',
-        filter: data.params.filter || null,
-      });
-    }
-
-    _handlePluginListRoute(data) {
-      this._setParams({
-        view: Gerrit.Nav.View.ADMIN,
-        adminView: 'gr-plugin-list',
-      });
-    }
-
-    _handleQueryRoute(data) {
-      this._setParams({
-        view: Gerrit.Nav.View.SEARCH,
-        query: data.params[0],
-        offset: data.params[2],
-      });
-    }
-
-    _handleQueryLegacySuffixRoute(ctx) {
-      this._redirect(ctx.path.replace(LEGACY_QUERY_SUFFIX_PATTERN, ''));
-    }
-
-    _handleChangeNumberLegacyRoute(ctx) {
-      this._redirect('/c/' + encodeURIComponent(ctx.params[0]));
-    }
-
-    _handleChangeRoute(ctx) {
-      // Parameter order is based on the regex group number matched.
-      const params = {
-        project: ctx.params[0],
-        changeNum: ctx.params[1],
-        basePatchNum: ctx.params[4],
-        patchNum: ctx.params[6],
-        view: Gerrit.Nav.View.CHANGE,
-      };
-
-      this.$.reporting.setRepoName(params.project);
-      this._redirectOrNavigate(params);
-    }
-
-    _handleDiffRoute(ctx) {
-      // Parameter order is based on the regex group number matched.
-      const params = {
-        project: ctx.params[0],
-        changeNum: ctx.params[1],
-        basePatchNum: ctx.params[4],
-        patchNum: ctx.params[6],
-        path: ctx.params[8],
-        view: Gerrit.Nav.View.DIFF,
-      };
-
-      const address = this._parseLineAddress(ctx.hash);
-      if (address) {
-        params.leftSide = address.leftSide;
-        params.lineNum = address.lineNum;
-      }
-      this.$.reporting.setRepoName(params.project);
-      this._redirectOrNavigate(params);
-    }
-
-    _handleChangeLegacyRoute(ctx) {
-      // Parameter order is based on the regex group number matched.
-      const params = {
-        changeNum: ctx.params[0],
-        basePatchNum: ctx.params[3],
-        patchNum: ctx.params[5],
-        view: Gerrit.Nav.View.CHANGE,
-        querystring: ctx.querystring,
-      };
-
-      this._normalizeLegacyRouteParams(params);
-    }
-
-    _handleLegacyLinenum(ctx) {
-      this._redirect(ctx.path.replace(LEGACY_LINENUM_PATTERN, '#$1'));
-    }
-
-    _handleDiffLegacyRoute(ctx) {
-      // Parameter order is based on the regex group number matched.
-      const params = {
-        changeNum: ctx.params[0],
-        basePatchNum: ctx.params[2],
-        patchNum: ctx.params[4],
-        path: ctx.params[5],
-        view: Gerrit.Nav.View.DIFF,
-      };
-
-      const address = this._parseLineAddress(ctx.hash);
-      if (address) {
-        params.leftSide = address.leftSide;
-        params.lineNum = address.lineNum;
-      }
-
-      this._normalizeLegacyRouteParams(params);
-    }
-
-    _handleDiffEditRoute(ctx) {
-      // Parameter order is based on the regex group number matched.
-      const project = ctx.params[0];
-      this._redirectOrNavigate({
-        project,
-        changeNum: ctx.params[1],
-        patchNum: ctx.params[2],
-        path: ctx.params[3],
-        lineNum: ctx.hash,
-        view: Gerrit.Nav.View.EDIT,
-      });
-      this.$.reporting.setRepoName(project);
-    }
-
-    _handleChangeEditRoute(ctx) {
-      // Parameter order is based on the regex group number matched.
-      const project = ctx.params[0];
-      this._redirectOrNavigate({
-        project,
-        changeNum: ctx.params[1],
-        patchNum: ctx.params[3],
-        view: Gerrit.Nav.View.CHANGE,
-        edit: true,
-      });
-      this.$.reporting.setRepoName(project);
-    }
-
-    /**
-     * Normalize the patch range params for a the change or diff view and
-     * redirect if URL upgrade is needed.
-     */
-    _redirectOrNavigate(params) {
-      const needsRedirect = this._normalizePatchRangeParams(params);
-      if (needsRedirect) {
-        this._redirect(this._generateUrl(params));
-      } else {
-        this._setParams(params);
+  _handleProjectsOldRoute(data) {
+    let params = '';
+    if (data.params[1]) {
+      params = encodeURIComponent(data.params[1]);
+      if (data.params[1].includes(',')) {
+        params =
+            encodeURIComponent(data.params[1]).replace('%2C', ',');
       }
     }
 
-    _handleAgreementsRoute() {
-      this._redirect('/settings/#Agreements');
+    this._redirect(`/admin/repos/${params}`);
+  }
+
+  _handleRepoCommandsRoute(data) {
+    const repo = data.params[0];
+    this._setParams({
+      view: Gerrit.Nav.View.REPO,
+      detail: Gerrit.Nav.RepoDetailView.COMMANDS,
+      repo,
+    });
+    this.$.reporting.setRepoName(repo);
+  }
+
+  _handleRepoAccessRoute(data) {
+    const repo = data.params[0];
+    this._setParams({
+      view: Gerrit.Nav.View.REPO,
+      detail: Gerrit.Nav.RepoDetailView.ACCESS,
+      repo,
+    });
+    this.$.reporting.setRepoName(repo);
+  }
+
+  _handleRepoDashboardsRoute(data) {
+    const repo = data.params[0];
+    this._setParams({
+      view: Gerrit.Nav.View.REPO,
+      detail: Gerrit.Nav.RepoDetailView.DASHBOARDS,
+      repo,
+    });
+    this.$.reporting.setRepoName(repo);
+  }
+
+  _handleBranchListOffsetRoute(data) {
+    this._setParams({
+      view: Gerrit.Nav.View.REPO,
+      detail: Gerrit.Nav.RepoDetailView.BRANCHES,
+      repo: data.params[0],
+      offset: data.params[2] || 0,
+      filter: null,
+    });
+  }
+
+  _handleBranchListFilterOffsetRoute(data) {
+    this._setParams({
+      view: Gerrit.Nav.View.REPO,
+      detail: Gerrit.Nav.RepoDetailView.BRANCHES,
+      repo: data.params.repo,
+      offset: data.params.offset,
+      filter: data.params.filter,
+    });
+  }
+
+  _handleBranchListFilterRoute(data) {
+    this._setParams({
+      view: Gerrit.Nav.View.REPO,
+      detail: Gerrit.Nav.RepoDetailView.BRANCHES,
+      repo: data.params.repo,
+      filter: data.params.filter || null,
+    });
+  }
+
+  _handleTagListOffsetRoute(data) {
+    this._setParams({
+      view: Gerrit.Nav.View.REPO,
+      detail: Gerrit.Nav.RepoDetailView.TAGS,
+      repo: data.params[0],
+      offset: data.params[2] || 0,
+      filter: null,
+    });
+  }
+
+  _handleTagListFilterOffsetRoute(data) {
+    this._setParams({
+      view: Gerrit.Nav.View.REPO,
+      detail: Gerrit.Nav.RepoDetailView.TAGS,
+      repo: data.params.repo,
+      offset: data.params.offset,
+      filter: data.params.filter,
+    });
+  }
+
+  _handleTagListFilterRoute(data) {
+    this._setParams({
+      view: Gerrit.Nav.View.REPO,
+      detail: Gerrit.Nav.RepoDetailView.TAGS,
+      repo: data.params.repo,
+      filter: data.params.filter || null,
+    });
+  }
+
+  _handleRepoListOffsetRoute(data) {
+    this._setParams({
+      view: Gerrit.Nav.View.ADMIN,
+      adminView: 'gr-repo-list',
+      offset: data.params[1] || 0,
+      filter: null,
+      openCreateModal: data.hash === 'create',
+    });
+  }
+
+  _handleRepoListFilterOffsetRoute(data) {
+    this._setParams({
+      view: Gerrit.Nav.View.ADMIN,
+      adminView: 'gr-repo-list',
+      offset: data.params.offset,
+      filter: data.params.filter,
+    });
+  }
+
+  _handleRepoListFilterRoute(data) {
+    this._setParams({
+      view: Gerrit.Nav.View.ADMIN,
+      adminView: 'gr-repo-list',
+      filter: data.params.filter || null,
+    });
+  }
+
+  _handleCreateProjectRoute(data) {
+    // Redirects the legacy route to the new route, which displays the project
+    // list with a hash 'create'.
+    this._redirect('/admin/repos#create');
+  }
+
+  _handleCreateGroupRoute(data) {
+    // Redirects the legacy route to the new route, which displays the group
+    // list with a hash 'create'.
+    this._redirect('/admin/groups#create');
+  }
+
+  _handleRepoRoute(data) {
+    const repo = data.params[0];
+    this._setParams({
+      view: Gerrit.Nav.View.REPO,
+      repo,
+    });
+    this.$.reporting.setRepoName(repo);
+  }
+
+  _handlePluginListOffsetRoute(data) {
+    this._setParams({
+      view: Gerrit.Nav.View.ADMIN,
+      adminView: 'gr-plugin-list',
+      offset: data.params[1] || 0,
+      filter: null,
+    });
+  }
+
+  _handlePluginListFilterOffsetRoute(data) {
+    this._setParams({
+      view: Gerrit.Nav.View.ADMIN,
+      adminView: 'gr-plugin-list',
+      offset: data.params.offset,
+      filter: data.params.filter,
+    });
+  }
+
+  _handlePluginListFilterRoute(data) {
+    this._setParams({
+      view: Gerrit.Nav.View.ADMIN,
+      adminView: 'gr-plugin-list',
+      filter: data.params.filter || null,
+    });
+  }
+
+  _handlePluginListRoute(data) {
+    this._setParams({
+      view: Gerrit.Nav.View.ADMIN,
+      adminView: 'gr-plugin-list',
+    });
+  }
+
+  _handleQueryRoute(data) {
+    this._setParams({
+      view: Gerrit.Nav.View.SEARCH,
+      query: data.params[0],
+      offset: data.params[2],
+    });
+  }
+
+  _handleQueryLegacySuffixRoute(ctx) {
+    this._redirect(ctx.path.replace(LEGACY_QUERY_SUFFIX_PATTERN, ''));
+  }
+
+  _handleChangeNumberLegacyRoute(ctx) {
+    this._redirect('/c/' + encodeURIComponent(ctx.params[0]));
+  }
+
+  _handleChangeRoute(ctx) {
+    // Parameter order is based on the regex group number matched.
+    const params = {
+      project: ctx.params[0],
+      changeNum: ctx.params[1],
+      basePatchNum: ctx.params[4],
+      patchNum: ctx.params[6],
+      view: Gerrit.Nav.View.CHANGE,
+    };
+
+    this.$.reporting.setRepoName(params.project);
+    this._redirectOrNavigate(params);
+  }
+
+  _handleDiffRoute(ctx) {
+    // Parameter order is based on the regex group number matched.
+    const params = {
+      project: ctx.params[0],
+      changeNum: ctx.params[1],
+      basePatchNum: ctx.params[4],
+      patchNum: ctx.params[6],
+      path: ctx.params[8],
+      view: Gerrit.Nav.View.DIFF,
+    };
+
+    const address = this._parseLineAddress(ctx.hash);
+    if (address) {
+      params.leftSide = address.leftSide;
+      params.lineNum = address.lineNum;
+    }
+    this.$.reporting.setRepoName(params.project);
+    this._redirectOrNavigate(params);
+  }
+
+  _handleChangeLegacyRoute(ctx) {
+    // Parameter order is based on the regex group number matched.
+    const params = {
+      changeNum: ctx.params[0],
+      basePatchNum: ctx.params[3],
+      patchNum: ctx.params[5],
+      view: Gerrit.Nav.View.CHANGE,
+      querystring: ctx.querystring,
+    };
+
+    this._normalizeLegacyRouteParams(params);
+  }
+
+  _handleLegacyLinenum(ctx) {
+    this._redirect(ctx.path.replace(LEGACY_LINENUM_PATTERN, '#$1'));
+  }
+
+  _handleDiffLegacyRoute(ctx) {
+    // Parameter order is based on the regex group number matched.
+    const params = {
+      changeNum: ctx.params[0],
+      basePatchNum: ctx.params[2],
+      patchNum: ctx.params[4],
+      path: ctx.params[5],
+      view: Gerrit.Nav.View.DIFF,
+    };
+
+    const address = this._parseLineAddress(ctx.hash);
+    if (address) {
+      params.leftSide = address.leftSide;
+      params.lineNum = address.lineNum;
     }
 
-    _handleNewAgreementsRoute(data) {
-      data.params.view = Gerrit.Nav.View.AGREEMENTS;
-      this._setParams(data.params);
-    }
+    this._normalizeLegacyRouteParams(params);
+  }
 
-    _handleSettingsLegacyRoute(data) {
-      // email tokens may contain '+' but no space.
-      // The parameter parsing replaces all '+' with a space,
-      // undo that to have valid tokens.
-      const token = data.params[0].replace(/ /g, '+');
-      this._setParams({
-        view: Gerrit.Nav.View.SETTINGS,
-        emailToken: token,
-      });
-    }
+  _handleDiffEditRoute(ctx) {
+    // Parameter order is based on the regex group number matched.
+    const project = ctx.params[0];
+    this._redirectOrNavigate({
+      project,
+      changeNum: ctx.params[1],
+      patchNum: ctx.params[2],
+      path: ctx.params[3],
+      lineNum: ctx.hash,
+      view: Gerrit.Nav.View.EDIT,
+    });
+    this.$.reporting.setRepoName(project);
+  }
 
-    _handleSettingsRoute(data) {
-      this._setParams({view: Gerrit.Nav.View.SETTINGS});
-    }
+  _handleChangeEditRoute(ctx) {
+    // Parameter order is based on the regex group number matched.
+    const project = ctx.params[0];
+    this._redirectOrNavigate({
+      project,
+      changeNum: ctx.params[1],
+      patchNum: ctx.params[3],
+      view: Gerrit.Nav.View.CHANGE,
+      edit: true,
+    });
+    this.$.reporting.setRepoName(project);
+  }
 
-    _handleRegisterRoute(ctx) {
-      this._setParams({justRegistered: true});
-      let path = ctx.params[0] || '/';
-
-      // Prevent redirect looping.
-      if (path.startsWith('/register')) { path = '/'; }
-
-      if (path[0] !== '/') { return; }
-      this._redirect(this.getBaseUrl() + path);
-    }
-
-    /**
-     * Handler for routes that should pass through the router and not be caught
-     * by the catchall _handleDefaultRoute handler.
-     */
-    _handlePassThroughRoute() {
-      location.reload();
-    }
-
-    /**
-     * URL may sometimes have /+/ encoded to / /.
-     * Context: Issue 6888, Issue 7100
-     */
-    _handleImproperlyEncodedPlusRoute(ctx) {
-      let hash = this._getHashFromCanonicalPath(ctx.canonicalPath);
-      if (hash.length) { hash = '#' + hash; }
-      this._redirect(`/c/${ctx.params[0]}/+/${ctx.params[1]}${hash}`);
-    }
-
-    _handlePluginScreen(ctx) {
-      const view = Gerrit.Nav.View.PLUGIN_SCREEN;
-      const plugin = ctx.params[0];
-      const screen = ctx.params[1];
-      this._setParams({view, plugin, screen});
-    }
-
-    _handleDocumentationSearchRoute(data) {
-      this._setParams({
-        view: Gerrit.Nav.View.DOCUMENTATION_SEARCH,
-        filter: data.params.filter || null,
-      });
-    }
-
-    _handleDocumentationSearchRedirectRoute(data) {
-      this._redirect('/Documentation/q/filter:' +
-          encodeURIComponent(data.params[0]));
-    }
-
-    _handleDocumentationRedirectRoute(data) {
-      if (data.params[1]) {
-        location.reload();
-      } else {
-        // Redirect /Documentation to /Documentation/index.html
-        this._redirect('/Documentation/index.html');
-      }
-    }
-
-    /**
-     * Catchall route for when no other route is matched.
-     */
-    _handleDefaultRoute() {
-      if (this._isInitialLoad) {
-        // Server recognized this route as polygerrit, so we show 404.
-        this._show404();
-      } else {
-        // Route can be recognized by server, so we pass it to server.
-        this._handlePassThroughRoute();
-      }
-    }
-
-    _show404() {
-      // Note: the app's 404 display is tightly-coupled with catching 404
-      // network responses, so we simulate a 404 response status to display it.
-      // TODO: Decouple the gr-app error view from network responses.
-      this._appElement().dispatchEvent(new CustomEvent('page-error',
-          {detail: {response: {status: 404}}}));
+  /**
+   * Normalize the patch range params for a the change or diff view and
+   * redirect if URL upgrade is needed.
+   */
+  _redirectOrNavigate(params) {
+    const needsRedirect = this._normalizePatchRangeParams(params);
+    if (needsRedirect) {
+      this._redirect(this._generateUrl(params));
+    } else {
+      this._setParams(params);
     }
   }
 
-  customElements.define(GrRouter.is, GrRouter);
-})();
+  _handleAgreementsRoute() {
+    this._redirect('/settings/#Agreements');
+  }
+
+  _handleNewAgreementsRoute(data) {
+    data.params.view = Gerrit.Nav.View.AGREEMENTS;
+    this._setParams(data.params);
+  }
+
+  _handleSettingsLegacyRoute(data) {
+    // email tokens may contain '+' but no space.
+    // The parameter parsing replaces all '+' with a space,
+    // undo that to have valid tokens.
+    const token = data.params[0].replace(/ /g, '+');
+    this._setParams({
+      view: Gerrit.Nav.View.SETTINGS,
+      emailToken: token,
+    });
+  }
+
+  _handleSettingsRoute(data) {
+    this._setParams({view: Gerrit.Nav.View.SETTINGS});
+  }
+
+  _handleRegisterRoute(ctx) {
+    this._setParams({justRegistered: true});
+    let path = ctx.params[0] || '/';
+
+    // Prevent redirect looping.
+    if (path.startsWith('/register')) { path = '/'; }
+
+    if (path[0] !== '/') { return; }
+    this._redirect(this.getBaseUrl() + path);
+  }
+
+  /**
+   * Handler for routes that should pass through the router and not be caught
+   * by the catchall _handleDefaultRoute handler.
+   */
+  _handlePassThroughRoute() {
+    location.reload();
+  }
+
+  /**
+   * URL may sometimes have /+/ encoded to / /.
+   * Context: Issue 6888, Issue 7100
+   */
+  _handleImproperlyEncodedPlusRoute(ctx) {
+    let hash = this._getHashFromCanonicalPath(ctx.canonicalPath);
+    if (hash.length) { hash = '#' + hash; }
+    this._redirect(`/c/${ctx.params[0]}/+/${ctx.params[1]}${hash}`);
+  }
+
+  _handlePluginScreen(ctx) {
+    const view = Gerrit.Nav.View.PLUGIN_SCREEN;
+    const plugin = ctx.params[0];
+    const screen = ctx.params[1];
+    this._setParams({view, plugin, screen});
+  }
+
+  _handleDocumentationSearchRoute(data) {
+    this._setParams({
+      view: Gerrit.Nav.View.DOCUMENTATION_SEARCH,
+      filter: data.params.filter || null,
+    });
+  }
+
+  _handleDocumentationSearchRedirectRoute(data) {
+    this._redirect('/Documentation/q/filter:' +
+        encodeURIComponent(data.params[0]));
+  }
+
+  _handleDocumentationRedirectRoute(data) {
+    if (data.params[1]) {
+      location.reload();
+    } else {
+      // Redirect /Documentation to /Documentation/index.html
+      this._redirect('/Documentation/index.html');
+    }
+  }
+
+  /**
+   * Catchall route for when no other route is matched.
+   */
+  _handleDefaultRoute() {
+    if (this._isInitialLoad) {
+      // Server recognized this route as polygerrit, so we show 404.
+      this._show404();
+    } else {
+      // Route can be recognized by server, so we pass it to server.
+      this._handlePassThroughRoute();
+    }
+  }
+
+  _show404() {
+    // Note: the app's 404 display is tightly-coupled with catching 404
+    // network responses, so we simulate a 404 response status to display it.
+    // TODO: Decouple the gr-app error view from network responses.
+    this._appElement().dispatchEvent(new CustomEvent('page-error',
+        {detail: {response: {status: 404}}}));
+  }
+}
+
+customElements.define(GrRouter.is, GrRouter);
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_html.js b/polygerrit-ui/app/elements/core/gr-router/gr-router_html.js
index 5d2531e..01acaa3 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router_html.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_html.js
@@ -1,34 +1,22 @@
-<!--
-@license
-Copyright (C) 2016 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.
+ */
+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.
--->
-<link rel="import" href="/bower_components/polymer/polymer.html">
-
-<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
-<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../gr-reporting/gr-reporting.html">
-<script src="/bower_components/page/page.js"></script>
-
-<dom-module id="gr-router">
-  <template>
+export const htmlTemplate = html`
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
     <gr-reporting id="reporting"></gr-reporting>
-  </template>
-  <script src="gr-router.js"></script>
-</dom-module>
+`;
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.html
index f127a91..6ea07a5 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
@@ -19,15 +19,20 @@
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-router</title>
 
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-router.html">
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
+<script type="module" src="../../../test/test-pre-setup.js"></script>
+<script type="module" src="../../../test/common-test-setup.js"></script>
+<script type="module" src="./gr-router.js"></script>
 
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-router.js';
+void(0);
+</script>
 
 <test-fixture id="basic">
   <template>
@@ -35,1617 +40,1619 @@
   </template>
 </test-fixture>
 
-<script>
-  suite('gr-router tests', async () => {
-    await readyToTest();
-    let element;
-    let sandbox;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-router.js';
+suite('gr-router tests', () => {
+  let element;
+  let sandbox;
+
+  setup(() => {
+    sandbox = sinon.sandbox.create();
+    element = fixture('basic');
+  });
+
+  teardown(() => { sandbox.restore(); });
+
+  test('_firstCodeBrowserWeblink', () => {
+    assert.deepEqual(element._firstCodeBrowserWeblink([
+      {name: 'gitweb'},
+      {name: 'gitiles'},
+      {name: 'browse'},
+      {name: 'test'}]), {name: 'gitiles'});
+
+    assert.deepEqual(element._firstCodeBrowserWeblink([
+      {name: 'gitweb'},
+      {name: 'test'}]), {name: 'gitweb'});
+  });
+
+  test('_getBrowseCommitWeblink', () => {
+    const browserLink = {name: 'browser', url: 'browser/url'};
+    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);
+
+    assert.deepEqual(element._getBrowseCommitWeblink(weblinks, config),
+        browserLink);
+
+    assert.deepEqual(element._getBrowseCommitWeblink(weblinks, {}), link);
+  });
+
+  test('_getChangeWeblinks', () => {
+    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);
+
+    assert.deepEqual(
+        element._getChangeWeblinks(mapLinksToConfig([link, browserLink]))[0],
+        {name: 'test', url: 'test/url'});
+
+    assert.deepEqual(element._getChangeWeblinks(mapLinksToConfig([link]))[0],
+        {name: 'test', url: 'test/url'});
+
+    link.url = 'https://' + link.url;
+    assert.deepEqual(element._getChangeWeblinks(mapLinksToConfig([link]))[0],
+        {name: 'test', url: 'https://test/url'});
+  });
+
+  test('_getHashFromCanonicalPath', () => {
+    let url = '/foo/bar';
+    let hash = element._getHashFromCanonicalPath(url);
+    assert.equal(hash, '');
+
+    url = '';
+    hash = element._getHashFromCanonicalPath(url);
+    assert.equal(hash, '');
+
+    url = '/foo#bar';
+    hash = element._getHashFromCanonicalPath(url);
+    assert.equal(hash, 'bar');
+
+    url = '/foo#bar#baz';
+    hash = element._getHashFromCanonicalPath(url);
+    assert.equal(hash, 'bar#baz');
+
+    url = '#foo#bar#baz';
+    hash = element._getHashFromCanonicalPath(url);
+    assert.equal(hash, 'foo#bar#baz');
+  });
+
+  suite('_parseLineAddress', () => {
+    test('returns null for empty and invalid hashes', () => {
+      let actual = element._parseLineAddress('');
+      assert.isNull(actual);
+
+      actual = element._parseLineAddress('foobar');
+      assert.isNull(actual);
+
+      actual = element._parseLineAddress('foo123');
+      assert.isNull(actual);
+
+      actual = element._parseLineAddress('123bar');
+      assert.isNull(actual);
+    });
+
+    test('parses correctly', () => {
+      let actual = element._parseLineAddress('1234');
+      assert.isOk(actual);
+      assert.equal(actual.lineNum, 1234);
+      assert.isFalse(actual.leftSide);
+
+      actual = element._parseLineAddress('a4');
+      assert.isOk(actual);
+      assert.equal(actual.lineNum, 4);
+      assert.isTrue(actual.leftSide);
+
+      actual = element._parseLineAddress('b77');
+      assert.isOk(actual);
+      assert.equal(actual.lineNum, 77);
+      assert.isTrue(actual.leftSide);
+    });
+  });
+
+  test('_startRouter requires auth for the right handlers', () => {
+    // This test encodes the lists of route handler methods that gr-router
+    // automatically checks for authentication before triggering.
+
+    const requiresAuth = {};
+    const doesNotRequireAuth = {};
+    sandbox.stub(Gerrit.Nav, 'setup');
+    sandbox.stub(window.page, 'start');
+    sandbox.stub(window.page, 'base');
+    sandbox.stub(window, 'page');
+    sandbox.stub(element, '_mapRoute', (pattern, methodName, usesAuth) => {
+      if (usesAuth) {
+        requiresAuth[methodName] = true;
+      } else {
+        doesNotRequireAuth[methodName] = true;
+      }
+    });
+    element._startRouter();
+
+    const actualRequiresAuth = Object.keys(requiresAuth);
+    actualRequiresAuth.sort();
+    const actualDoesNotRequireAuth = Object.keys(doesNotRequireAuth);
+    actualDoesNotRequireAuth.sort();
+
+    const shouldRequireAutoAuth = [
+      '_handleAgreementsRoute',
+      '_handleChangeEditRoute',
+      '_handleCreateGroupRoute',
+      '_handleCreateProjectRoute',
+      '_handleDiffEditRoute',
+      '_handleGroupAuditLogRoute',
+      '_handleGroupInfoRoute',
+      '_handleGroupListFilterOffsetRoute',
+      '_handleGroupListFilterRoute',
+      '_handleGroupListOffsetRoute',
+      '_handleGroupMembersRoute',
+      '_handleGroupRoute',
+      '_handleGroupSelfRedirectRoute',
+      '_handleNewAgreementsRoute',
+      '_handlePluginListFilterOffsetRoute',
+      '_handlePluginListFilterRoute',
+      '_handlePluginListOffsetRoute',
+      '_handlePluginListRoute',
+      '_handleRepoCommandsRoute',
+      '_handleSettingsLegacyRoute',
+      '_handleSettingsRoute',
+    ];
+    assert.deepEqual(actualRequiresAuth, shouldRequireAutoAuth);
+
+    const unauthenticatedHandlers = [
+      '_handleBranchListFilterOffsetRoute',
+      '_handleBranchListFilterRoute',
+      '_handleBranchListOffsetRoute',
+      '_handleChangeNumberLegacyRoute',
+      '_handleChangeRoute',
+      '_handleDiffRoute',
+      '_handleDefaultRoute',
+      '_handleChangeLegacyRoute',
+      '_handleDiffLegacyRoute',
+      '_handleDocumentationRedirectRoute',
+      '_handleDocumentationSearchRoute',
+      '_handleDocumentationSearchRedirectRoute',
+      '_handleLegacyLinenum',
+      '_handleImproperlyEncodedPlusRoute',
+      '_handlePassThroughRoute',
+      '_handleProjectDashboardRoute',
+      '_handleProjectsOldRoute',
+      '_handleRepoAccessRoute',
+      '_handleRepoDashboardsRoute',
+      '_handleRepoListFilterOffsetRoute',
+      '_handleRepoListFilterRoute',
+      '_handleRepoListOffsetRoute',
+      '_handleRepoRoute',
+      '_handleQueryLegacySuffixRoute',
+      '_handleQueryRoute',
+      '_handleRegisterRoute',
+      '_handleTagListFilterOffsetRoute',
+      '_handleTagListFilterRoute',
+      '_handleTagListOffsetRoute',
+      '_handlePluginScreen',
+    ];
+
+    // Handler names that check authentication themselves, and thus don't need
+    // it performed for them.
+    const selfAuthenticatingHandlers = [
+      '_handleDashboardRoute',
+      '_handleCustomDashboardRoute',
+      '_handleRootRoute',
+    ];
+
+    const shouldNotRequireAuth = unauthenticatedHandlers
+        .concat(selfAuthenticatingHandlers);
+    shouldNotRequireAuth.sort();
+    assert.deepEqual(actualDoesNotRequireAuth, shouldNotRequireAuth);
+  });
+
+  test('_redirectIfNotLoggedIn while logged in', () => {
+    sandbox.stub(element.$.restAPI, 'getLoggedIn')
+        .returns(Promise.resolve(true));
+    const data = {canonicalPath: ''};
+    const redirectStub = sandbox.stub(element, '_redirectToLogin');
+    return element._redirectIfNotLoggedIn(data).then(() => {
+      assert.isFalse(redirectStub.called);
+    });
+  });
+
+  test('_redirectIfNotLoggedIn while logged out', () => {
+    sandbox.stub(element.$.restAPI, 'getLoggedIn')
+        .returns(Promise.resolve(false));
+    const redirectStub = sandbox.stub(element, '_redirectToLogin');
+    const data = {canonicalPath: ''};
+    return new Promise(resolve => {
+      element._redirectIfNotLoggedIn(data)
+          .then(() => {
+            assert.isTrue(false, 'Should never execute');
+          })
+          .catch(() => {
+            assert.isTrue(redirectStub.calledOnce);
+            resolve();
+          });
+    });
+  });
+
+  suite('generateUrl', () => {
+    test('search', () => {
+      let params = {
+        view: Gerrit.Nav.View.SEARCH,
+        owner: 'a%b',
+        project: 'c%d',
+        branch: 'e%f',
+        topic: 'g%h',
+        statuses: ['op%en'],
+      };
+      assert.equal(element._generateUrl(params),
+          '/q/owner:a%2525b+project:c%2525d+branch:e%2525f+' +
+          'topic:"g%2525h"+status:op%2525en');
+
+      params.offset = 100;
+      assert.equal(element._generateUrl(params),
+          '/q/owner:a%2525b+project:c%2525d+branch:e%2525f+' +
+          'topic:"g%2525h"+status:op%2525en,100');
+      delete params.offset;
+
+      // The presence of the query param overrides other params.
+      params.query = 'foo$bar';
+      assert.equal(element._generateUrl(params), '/q/foo%2524bar');
+
+      params.offset = 100;
+      assert.equal(element._generateUrl(params), '/q/foo%2524bar,100');
+
+      params = {
+        view: Gerrit.Nav.View.SEARCH,
+        statuses: ['a', 'b', 'c'],
+      };
+      assert.equal(element._generateUrl(params),
+          '/q/(status:a OR status:b OR status:c)');
+    });
+
+    test('change', () => {
+      const params = {
+        view: Gerrit.Nav.View.CHANGE,
+        changeNum: '1234',
+        project: 'test',
+      };
+      const paramsWithQuery = {
+        view: Gerrit.Nav.View.CHANGE,
+        changeNum: '1234',
+        project: 'test',
+        querystring: 'revert&foo=bar',
+      };
+
+      assert.equal(element._generateUrl(params), '/c/test/+/1234');
+      assert.equal(element._generateUrl(paramsWithQuery),
+          '/c/test/+/1234?revert&foo=bar');
+
+      params.patchNum = 10;
+      assert.equal(element._generateUrl(params), '/c/test/+/1234/10');
+      paramsWithQuery.patchNum = 10;
+      assert.equal(element._generateUrl(paramsWithQuery),
+          '/c/test/+/1234/10?revert&foo=bar');
+
+      params.basePatchNum = 5;
+      assert.equal(element._generateUrl(params), '/c/test/+/1234/5..10');
+      paramsWithQuery.basePatchNum = 5;
+      assert.equal(element._generateUrl(paramsWithQuery),
+          '/c/test/+/1234/5..10?revert&foo=bar');
+
+      params.messageHash = '#123';
+      assert.equal(element._generateUrl(params), '/c/test/+/1234/5..10#123');
+    });
+
+    test('change with repo name encoding', () => {
+      const params = {
+        view: Gerrit.Nav.View.CHANGE,
+        changeNum: '1234',
+        project: 'x+/y+/z+/w',
+      };
+      assert.equal(element._generateUrl(params),
+          '/c/x%252B/y%252B/z%252B/w/+/1234');
+    });
+
+    test('diff', () => {
+      const params = {
+        view: Gerrit.Nav.View.DIFF,
+        changeNum: '42',
+        path: 'x+y/path.cpp',
+        patchNum: 12,
+      };
+      assert.equal(element._generateUrl(params),
+          '/c/42/12/x%252By/path.cpp');
+
+      params.project = 'test';
+      assert.equal(element._generateUrl(params),
+          '/c/test/+/42/12/x%252By/path.cpp');
+
+      params.basePatchNum = 6;
+      assert.equal(element._generateUrl(params),
+          '/c/test/+/42/6..12/x%252By/path.cpp');
+
+      params.path = 'foo bar/my+file.txt%';
+      params.patchNum = 2;
+      delete params.basePatchNum;
+      assert.equal(element._generateUrl(params),
+          '/c/test/+/42/2/foo+bar/my%252Bfile.txt%2525');
+
+      params.path = 'file.cpp';
+      params.lineNum = 123;
+      assert.equal(element._generateUrl(params),
+          '/c/test/+/42/2/file.cpp#123');
+
+      params.leftSide = true;
+      assert.equal(element._generateUrl(params),
+          '/c/test/+/42/2/file.cpp#b123');
+    });
+
+    test('diff with repo name encoding', () => {
+      const params = {
+        view: Gerrit.Nav.View.DIFF,
+        changeNum: '42',
+        path: 'x+y/path.cpp',
+        patchNum: 12,
+        project: 'x+/y',
+      };
+      assert.equal(element._generateUrl(params),
+          '/c/x%252B/y/+/42/12/x%252By/path.cpp');
+    });
+
+    test('edit', () => {
+      const params = {
+        view: Gerrit.Nav.View.EDIT,
+        changeNum: '42',
+        project: 'test',
+        path: 'x+y/path.cpp',
+      };
+      assert.equal(element._generateUrl(params),
+          '/c/test/+/42/x%252By/path.cpp,edit');
+    });
+
+    test('_getPatchRangeExpression', () => {
+      const params = {};
+      let actual = element._getPatchRangeExpression(params);
+      assert.equal(actual, '');
+
+      params.patchNum = 4;
+      actual = element._getPatchRangeExpression(params);
+      assert.equal(actual, '4');
+
+      params.basePatchNum = 2;
+      actual = element._getPatchRangeExpression(params);
+      assert.equal(actual, '2..4');
+
+      delete params.patchNum;
+      actual = element._getPatchRangeExpression(params);
+      assert.equal(actual, '2..');
+    });
+
+    suite('dashboard', () => {
+      test('self dashboard', () => {
+        const params = {
+          view: Gerrit.Nav.View.DASHBOARD,
+        };
+        assert.equal(element._generateUrl(params), '/dashboard/self');
+      });
+
+      test('user dashboard', () => {
+        const params = {
+          view: Gerrit.Nav.View.DASHBOARD,
+          user: 'user',
+        };
+        assert.equal(element._generateUrl(params), '/dashboard/user');
+      });
+
+      test('custom self dashboard, no title', () => {
+        const params = {
+          view: Gerrit.Nav.View.DASHBOARD,
+          sections: [
+            {name: 'section 1', query: 'query 1'},
+            {name: 'section 2', query: 'query 2'},
+          ],
+        };
+        assert.equal(
+            element._generateUrl(params),
+            '/dashboard/?section%201=query%201&section%202=query%202');
+      });
+
+      test('custom repo dashboard', () => {
+        const params = {
+          view: Gerrit.Nav.View.DASHBOARD,
+          sections: [
+            {name: 'section 1', query: 'query 1 ${project}'},
+            {name: 'section 2', query: 'query 2 ${repo}'},
+          ],
+          repo: 'repo-name',
+        };
+        assert.equal(
+            element._generateUrl(params),
+            '/dashboard/?section%201=query%201%20repo-name&' +
+            'section%202=query%202%20repo-name');
+      });
+
+      test('custom user dashboard, with title', () => {
+        const params = {
+          view: Gerrit.Nav.View.DASHBOARD,
+          user: 'user',
+          sections: [{name: 'name', query: 'query'}],
+          title: 'custom dashboard',
+        };
+        assert.equal(
+            element._generateUrl(params),
+            '/dashboard/user?name=query&title=custom%20dashboard');
+      });
+
+      test('repo dashboard', () => {
+        const params = {
+          view: Gerrit.Nav.View.DASHBOARD,
+          repo: 'gerrit/repo',
+          dashboard: 'default:main',
+        };
+        assert.equal(
+            element._generateUrl(params),
+            '/p/gerrit/repo/+/dashboard/default:main');
+      });
+
+      test('project dashboard (legacy)', () => {
+        const params = {
+          view: Gerrit.Nav.View.DASHBOARD,
+          project: 'gerrit/project',
+          dashboard: 'default:main',
+        };
+        assert.equal(
+            element._generateUrl(params),
+            '/p/gerrit/project/+/dashboard/default:main');
+      });
+    });
+
+    suite('groups', () => {
+      test('group info', () => {
+        const params = {
+          view: Gerrit.Nav.View.GROUP,
+          groupId: 1234,
+        };
+        assert.equal(element._generateUrl(params), '/admin/groups/1234');
+      });
+
+      test('group members', () => {
+        const params = {
+          view: Gerrit.Nav.View.GROUP,
+          groupId: 1234,
+          detail: 'members',
+        };
+        assert.equal(element._generateUrl(params),
+            '/admin/groups/1234,members');
+      });
+
+      test('group audit log', () => {
+        const params = {
+          view: Gerrit.Nav.View.GROUP,
+          groupId: 1234,
+          detail: 'log',
+        };
+        assert.equal(element._generateUrl(params),
+            '/admin/groups/1234,audit-log');
+      });
+    });
+  });
+
+  suite('param normalization', () => {
+    let projectLookupStub;
 
     setup(() => {
-      sandbox = sinon.sandbox.create();
-      element = fixture('basic');
+      projectLookupStub = sandbox
+          .stub(element.$.restAPI, 'getFromProjectLookup');
+      sandbox.stub(element, '_generateUrl');
     });
 
-    teardown(() => { sandbox.restore(); });
+    suite('_normalizeLegacyRouteParams', () => {
+      let rangeStub;
+      let redirectStub;
+      let show404Stub;
 
-    test('_firstCodeBrowserWeblink', () => {
-      assert.deepEqual(element._firstCodeBrowserWeblink([
-        {name: 'gitweb'},
-        {name: 'gitiles'},
-        {name: 'browse'},
-        {name: 'test'}]), {name: 'gitiles'});
-
-      assert.deepEqual(element._firstCodeBrowserWeblink([
-        {name: 'gitweb'},
-        {name: 'test'}]), {name: 'gitweb'});
-    });
-
-    test('_getBrowseCommitWeblink', () => {
-      const browserLink = {name: 'browser', url: 'browser/url'};
-      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);
-
-      assert.deepEqual(element._getBrowseCommitWeblink(weblinks, config),
-          browserLink);
-
-      assert.deepEqual(element._getBrowseCommitWeblink(weblinks, {}), link);
-    });
-
-    test('_getChangeWeblinks', () => {
-      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);
-
-      assert.deepEqual(
-          element._getChangeWeblinks(mapLinksToConfig([link, browserLink]))[0],
-          {name: 'test', url: 'test/url'});
-
-      assert.deepEqual(element._getChangeWeblinks(mapLinksToConfig([link]))[0],
-          {name: 'test', url: 'test/url'});
-
-      link.url = 'https://' + link.url;
-      assert.deepEqual(element._getChangeWeblinks(mapLinksToConfig([link]))[0],
-          {name: 'test', url: 'https://test/url'});
-    });
-
-    test('_getHashFromCanonicalPath', () => {
-      let url = '/foo/bar';
-      let hash = element._getHashFromCanonicalPath(url);
-      assert.equal(hash, '');
-
-      url = '';
-      hash = element._getHashFromCanonicalPath(url);
-      assert.equal(hash, '');
-
-      url = '/foo#bar';
-      hash = element._getHashFromCanonicalPath(url);
-      assert.equal(hash, 'bar');
-
-      url = '/foo#bar#baz';
-      hash = element._getHashFromCanonicalPath(url);
-      assert.equal(hash, 'bar#baz');
-
-      url = '#foo#bar#baz';
-      hash = element._getHashFromCanonicalPath(url);
-      assert.equal(hash, 'foo#bar#baz');
-    });
-
-    suite('_parseLineAddress', () => {
-      test('returns null for empty and invalid hashes', () => {
-        let actual = element._parseLineAddress('');
-        assert.isNull(actual);
-
-        actual = element._parseLineAddress('foobar');
-        assert.isNull(actual);
-
-        actual = element._parseLineAddress('foo123');
-        assert.isNull(actual);
-
-        actual = element._parseLineAddress('123bar');
-        assert.isNull(actual);
+      setup(() => {
+        rangeStub = sandbox.stub(element, '_normalizePatchRangeParams')
+            .returns(Promise.resolve());
+        redirectStub = sandbox.stub(element, '_redirect');
+        show404Stub = sandbox.stub(element, '_show404');
       });
 
-      test('parses correctly', () => {
-        let actual = element._parseLineAddress('1234');
-        assert.isOk(actual);
-        assert.equal(actual.lineNum, 1234);
-        assert.isFalse(actual.leftSide);
+      test('w/o changeNum', () => {
+        projectLookupStub.returns(Promise.resolve('foo/bar'));
+        const params = {};
+        return element._normalizeLegacyRouteParams(params).then(() => {
+          assert.isFalse(projectLookupStub.called);
+          assert.isFalse(rangeStub.called);
+          assert.isNotOk(params.project);
+          assert.isFalse(redirectStub.called);
+          assert.isFalse(show404Stub.called);
+        });
+      });
 
-        actual = element._parseLineAddress('a4');
-        assert.isOk(actual);
-        assert.equal(actual.lineNum, 4);
-        assert.isTrue(actual.leftSide);
+      test('w/ changeNum', () => {
+        projectLookupStub.returns(Promise.resolve('foo/bar'));
+        const params = {changeNum: 1234};
+        return element._normalizeLegacyRouteParams(params).then(() => {
+          assert.isTrue(projectLookupStub.called);
+          assert.isTrue(rangeStub.called);
+          assert.equal(params.project, 'foo/bar');
+          assert.isTrue(redirectStub.calledOnce);
+          assert.isFalse(show404Stub.called);
+        });
+      });
 
-        actual = element._parseLineAddress('b77');
-        assert.isOk(actual);
-        assert.equal(actual.lineNum, 77);
-        assert.isTrue(actual.leftSide);
+      test('halts on project lookup failure', () => {
+        projectLookupStub.returns(Promise.resolve(undefined));
+        const params = {changeNum: 1234};
+        return element._normalizeLegacyRouteParams(params).then(() => {
+          assert.isTrue(projectLookupStub.called);
+          assert.isFalse(rangeStub.called);
+          assert.isUndefined(params.project);
+          assert.isFalse(redirectStub.called);
+          assert.isTrue(show404Stub.calledOnce);
+        });
       });
     });
 
-    test('_startRouter requires auth for the right handlers', () => {
-      // This test encodes the lists of route handler methods that gr-router
-      // automatically checks for authentication before triggering.
+    suite('_normalizePatchRangeParams', () => {
+      test('range n..n normalizes to n', () => {
+        const params = {basePatchNum: 4, patchNum: 4};
+        const needsRedirect = element._normalizePatchRangeParams(params);
+        assert.isTrue(needsRedirect);
+        assert.isNotOk(params.basePatchNum);
+        assert.equal(params.patchNum, 4);
+      });
 
-      const requiresAuth = {};
-      const doesNotRequireAuth = {};
+      test('range n.. normalizes to n', () => {
+        const params = {basePatchNum: 4};
+        const needsRedirect = element._normalizePatchRangeParams(params);
+        assert.isFalse(needsRedirect);
+        assert.isNotOk(params.basePatchNum);
+        assert.equal(params.patchNum, 4);
+      });
+    });
+  });
+
+  suite('route handlers', () => {
+    let redirectStub;
+    let setParamsStub;
+    let handlePassThroughRoute;
+
+    // Simple route handlers are direct mappings from parsed route data to a
+    // new set of app.params. This test helper asserts that passing `data`
+    // into `methodName` results in setting the params specified in `params`.
+    function assertDataToParams(data, methodName, params) {
+      element[methodName](data);
+      assert.deepEqual(setParamsStub.lastCall.args[0], params);
+    }
+
+    setup(() => {
+      redirectStub = sandbox.stub(element, '_redirect');
+      setParamsStub = sandbox.stub(element, '_setParams');
+      handlePassThroughRoute = sandbox.stub(element, '_handlePassThroughRoute');
+    });
+
+    test('_handleAgreementsRoute', () => {
+      const data = {params: {}};
+      element._handleAgreementsRoute(data);
+      assert.isTrue(redirectStub.calledOnce);
+      assert.equal(redirectStub.lastCall.args[0], '/settings/#Agreements');
+    });
+
+    test('_handleNewAgreementsRoute', () => {
+      element._handleNewAgreementsRoute({params: {}});
+      assert.isTrue(setParamsStub.calledOnce);
+      assert.equal(setParamsStub.lastCall.args[0].view,
+          Gerrit.Nav.View.AGREEMENTS);
+    });
+
+    test('_handleSettingsLegacyRoute', () => {
+      const data = {params: {0: 'my-token'}};
+      assertDataToParams(data, '_handleSettingsLegacyRoute', {
+        view: Gerrit.Nav.View.SETTINGS,
+        emailToken: 'my-token',
+      });
+    });
+
+    test('_handleSettingsLegacyRoute with +', () => {
+      const data = {params: {0: 'my-token test'}};
+      assertDataToParams(data, '_handleSettingsLegacyRoute', {
+        view: Gerrit.Nav.View.SETTINGS,
+        emailToken: 'my-token+test',
+      });
+    });
+
+    test('_handleSettingsRoute', () => {
+      const data = {};
+      assertDataToParams(data, '_handleSettingsRoute', {
+        view: Gerrit.Nav.View.SETTINGS,
+      });
+    });
+
+    test('_handleDefaultRoute on first load', () => {
+      const appElementStub = {dispatchEvent: sinon.stub()};
+      element._appElement = () => appElementStub;
+      element._handleDefaultRoute();
+      assert.isTrue(appElementStub.dispatchEvent.calledOnce);
+      assert.equal(
+          appElementStub.dispatchEvent.lastCall.args[0].detail.response.status,
+          404);
+    });
+
+    test('_handleDefaultRoute after internal navigation', () => {
+      let onExit = null;
+      const onRegisteringExit = (match, _onExit) => {
+        onExit = _onExit;
+      };
+      sandbox.stub(window.page, 'exit', onRegisteringExit);
       sandbox.stub(Gerrit.Nav, 'setup');
       sandbox.stub(window.page, 'start');
       sandbox.stub(window.page, 'base');
       sandbox.stub(window, 'page');
-      sandbox.stub(element, '_mapRoute', (pattern, methodName, usesAuth) => {
-        if (usesAuth) {
-          requiresAuth[methodName] = true;
-        } else {
-          doesNotRequireAuth[methodName] = true;
-        }
-      });
       element._startRouter();
 
-      const actualRequiresAuth = Object.keys(requiresAuth);
-      actualRequiresAuth.sort();
-      const actualDoesNotRequireAuth = Object.keys(doesNotRequireAuth);
-      actualDoesNotRequireAuth.sort();
+      const appElementStub = {dispatchEvent: sinon.stub()};
+      element._appElement = () => appElementStub;
+      element._handleDefaultRoute();
 
-      const shouldRequireAutoAuth = [
-        '_handleAgreementsRoute',
-        '_handleChangeEditRoute',
-        '_handleCreateGroupRoute',
-        '_handleCreateProjectRoute',
-        '_handleDiffEditRoute',
-        '_handleGroupAuditLogRoute',
-        '_handleGroupInfoRoute',
-        '_handleGroupListFilterOffsetRoute',
-        '_handleGroupListFilterRoute',
-        '_handleGroupListOffsetRoute',
-        '_handleGroupMembersRoute',
-        '_handleGroupRoute',
-        '_handleGroupSelfRedirectRoute',
-        '_handleNewAgreementsRoute',
-        '_handlePluginListFilterOffsetRoute',
-        '_handlePluginListFilterRoute',
-        '_handlePluginListOffsetRoute',
-        '_handlePluginListRoute',
-        '_handleRepoCommandsRoute',
-        '_handleSettingsLegacyRoute',
-        '_handleSettingsRoute',
-      ];
-      assert.deepEqual(actualRequiresAuth, shouldRequireAutoAuth);
+      onExit('', () => {}); // we left page;
 
-      const unauthenticatedHandlers = [
-        '_handleBranchListFilterOffsetRoute',
-        '_handleBranchListFilterRoute',
-        '_handleBranchListOffsetRoute',
-        '_handleChangeNumberLegacyRoute',
-        '_handleChangeRoute',
-        '_handleDiffRoute',
-        '_handleDefaultRoute',
-        '_handleChangeLegacyRoute',
-        '_handleDiffLegacyRoute',
-        '_handleDocumentationRedirectRoute',
-        '_handleDocumentationSearchRoute',
-        '_handleDocumentationSearchRedirectRoute',
-        '_handleLegacyLinenum',
-        '_handleImproperlyEncodedPlusRoute',
-        '_handlePassThroughRoute',
-        '_handleProjectDashboardRoute',
-        '_handleProjectsOldRoute',
-        '_handleRepoAccessRoute',
-        '_handleRepoDashboardsRoute',
-        '_handleRepoListFilterOffsetRoute',
-        '_handleRepoListFilterRoute',
-        '_handleRepoListOffsetRoute',
-        '_handleRepoRoute',
-        '_handleQueryLegacySuffixRoute',
-        '_handleQueryRoute',
-        '_handleRegisterRoute',
-        '_handleTagListFilterOffsetRoute',
-        '_handleTagListFilterRoute',
-        '_handleTagListOffsetRoute',
-        '_handlePluginScreen',
-      ];
-
-      // Handler names that check authentication themselves, and thus don't need
-      // it performed for them.
-      const selfAuthenticatingHandlers = [
-        '_handleDashboardRoute',
-        '_handleCustomDashboardRoute',
-        '_handleRootRoute',
-      ];
-
-      const shouldNotRequireAuth = unauthenticatedHandlers
-          .concat(selfAuthenticatingHandlers);
-      shouldNotRequireAuth.sort();
-      assert.deepEqual(actualDoesNotRequireAuth, shouldNotRequireAuth);
+      element._handleDefaultRoute();
+      assert.isTrue(handlePassThroughRoute.calledOnce);
     });
 
-    test('_redirectIfNotLoggedIn while logged in', () => {
-      sandbox.stub(element.$.restAPI, 'getLoggedIn')
-          .returns(Promise.resolve(true));
-      const data = {canonicalPath: ''};
-      const redirectStub = sandbox.stub(element, '_redirectToLogin');
-      return element._redirectIfNotLoggedIn(data).then(() => {
+    test('_handleImproperlyEncodedPlusRoute', () => {
+      // Regression test for Issue 7100.
+      element._handleImproperlyEncodedPlusRoute(
+          {canonicalPath: '/c/test/%20/42', params: ['test', '42']});
+      assert.isTrue(redirectStub.calledOnce);
+      assert.equal(
+          redirectStub.lastCall.args[0],
+          '/c/test/+/42');
+
+      sandbox.stub(element, '_getHashFromCanonicalPath').returns('foo');
+      element._handleImproperlyEncodedPlusRoute(
+          {canonicalPath: '/c/test/%20/42', params: ['test', '42']});
+      assert.equal(
+          redirectStub.lastCall.args[0],
+          '/c/test/+/42#foo');
+    });
+
+    test('_handleQueryRoute', () => {
+      const data = {params: ['project:foo/bar/baz']};
+      assertDataToParams(data, '_handleQueryRoute', {
+        view: Gerrit.Nav.View.SEARCH,
+        query: 'project:foo/bar/baz',
+        offset: undefined,
+      });
+
+      data.params.push(',123', '123');
+      assertDataToParams(data, '_handleQueryRoute', {
+        view: Gerrit.Nav.View.SEARCH,
+        query: 'project:foo/bar/baz',
+        offset: '123',
+      });
+    });
+
+    test('_handleQueryLegacySuffixRoute', () => {
+      element._handleQueryLegacySuffixRoute({path: '/q/foo+bar,n,z'});
+      assert.isTrue(redirectStub.calledOnce);
+      assert.equal(redirectStub.lastCall.args[0], '/q/foo+bar');
+    });
+
+    test('_handleQueryRoute', () => {
+      const data = {params: ['project:foo/bar/baz']};
+      assertDataToParams(data, '_handleQueryRoute', {
+        view: Gerrit.Nav.View.SEARCH,
+        query: 'project:foo/bar/baz',
+        offset: undefined,
+      });
+
+      data.params.push(',123', '123');
+      assertDataToParams(data, '_handleQueryRoute', {
+        view: Gerrit.Nav.View.SEARCH,
+        query: 'project:foo/bar/baz',
+        offset: '123',
+      });
+    });
+
+    suite('_handleRegisterRoute', () => {
+      test('happy path', () => {
+        const ctx = {params: ['/foo/bar']};
+        element._handleRegisterRoute(ctx);
+        assert.isTrue(redirectStub.calledWithExactly('/foo/bar'));
+        assert.isTrue(setParamsStub.calledOnce);
+        assert.isTrue(setParamsStub.lastCall.args[0].justRegistered);
+      });
+
+      test('no param', () => {
+        const ctx = {params: ['']};
+        element._handleRegisterRoute(ctx);
+        assert.isTrue(redirectStub.calledWithExactly('/'));
+        assert.isTrue(setParamsStub.calledOnce);
+        assert.isTrue(setParamsStub.lastCall.args[0].justRegistered);
+      });
+
+      test('prevent redirect', () => {
+        const ctx = {params: ['/register']};
+        element._handleRegisterRoute(ctx);
+        assert.isTrue(redirectStub.calledWithExactly('/'));
+        assert.isTrue(setParamsStub.calledOnce);
+        assert.isTrue(setParamsStub.lastCall.args[0].justRegistered);
+      });
+    });
+
+    suite('_handleRootRoute', () => {
+      test('closes for closeAfterLogin', () => {
+        const data = {querystring: 'closeAfterLogin', canonicalPath: ''};
+        const closeStub = sandbox.stub(window, 'close');
+        const result = element._handleRootRoute(data);
+        assert.isNotOk(result);
+        assert.isTrue(closeStub.called);
         assert.isFalse(redirectStub.called);
       });
-    });
 
-    test('_redirectIfNotLoggedIn while logged out', () => {
-      sandbox.stub(element.$.restAPI, 'getLoggedIn')
-          .returns(Promise.resolve(false));
-      const redirectStub = sandbox.stub(element, '_redirectToLogin');
-      const data = {canonicalPath: ''};
-      return new Promise(resolve => {
-        element._redirectIfNotLoggedIn(data)
-            .then(() => {
-              assert.isTrue(false, 'Should never execute');
-            })
-            .catch(() => {
-              assert.isTrue(redirectStub.calledOnce);
-              resolve();
-            });
-      });
-    });
-
-    suite('generateUrl', () => {
-      test('search', () => {
-        let params = {
-          view: Gerrit.Nav.View.SEARCH,
-          owner: 'a%b',
-          project: 'c%d',
-          branch: 'e%f',
-          topic: 'g%h',
-          statuses: ['op%en'],
+      test('redirects to dashboard if logged in', () => {
+        sandbox.stub(element.$.restAPI, 'getLoggedIn')
+            .returns(Promise.resolve(true));
+        const data = {
+          canonicalPath: '/', path: '/', querystring: '', hash: '',
         };
-        assert.equal(element._generateUrl(params),
-            '/q/owner:a%2525b+project:c%2525d+branch:e%2525f+' +
-            'topic:"g%2525h"+status:op%2525en');
+        const result = element._handleRootRoute(data);
+        assert.isOk(result);
+        return result.then(() => {
+          assert.isTrue(redirectStub.calledWithExactly('/dashboard/self'));
+        });
+      });
 
-        params.offset = 100;
-        assert.equal(element._generateUrl(params),
-            '/q/owner:a%2525b+project:c%2525d+branch:e%2525f+' +
-            'topic:"g%2525h"+status:op%2525en,100');
-        delete params.offset;
-
-        // The presence of the query param overrides other params.
-        params.query = 'foo$bar';
-        assert.equal(element._generateUrl(params), '/q/foo%2524bar');
-
-        params.offset = 100;
-        assert.equal(element._generateUrl(params), '/q/foo%2524bar,100');
-
-        params = {
-          view: Gerrit.Nav.View.SEARCH,
-          statuses: ['a', 'b', 'c'],
+      test('redirects to open changes if not logged in', () => {
+        sandbox.stub(element.$.restAPI, 'getLoggedIn')
+            .returns(Promise.resolve(false));
+        const data = {
+          canonicalPath: '/', path: '/', querystring: '', hash: '',
         };
-        assert.equal(element._generateUrl(params),
-            '/q/(status:a OR status:b OR status:c)');
+        const result = element._handleRootRoute(data);
+        assert.isOk(result);
+        return result.then(() => {
+          assert.isTrue(redirectStub.calledWithExactly('/q/status:open'));
+        });
       });
 
-      test('change', () => {
-        const params = {
-          view: Gerrit.Nav.View.CHANGE,
-          changeNum: '1234',
-          project: 'test',
-        };
-        const paramsWithQuery = {
-          view: Gerrit.Nav.View.CHANGE,
-          changeNum: '1234',
-          project: 'test',
-          querystring: 'revert&foo=bar',
-        };
-
-        assert.equal(element._generateUrl(params), '/c/test/+/1234');
-        assert.equal(element._generateUrl(paramsWithQuery),
-            '/c/test/+/1234?revert&foo=bar');
-
-        params.patchNum = 10;
-        assert.equal(element._generateUrl(params), '/c/test/+/1234/10');
-        paramsWithQuery.patchNum = 10;
-        assert.equal(element._generateUrl(paramsWithQuery),
-            '/c/test/+/1234/10?revert&foo=bar');
-
-        params.basePatchNum = 5;
-        assert.equal(element._generateUrl(params), '/c/test/+/1234/5..10');
-        paramsWithQuery.basePatchNum = 5;
-        assert.equal(element._generateUrl(paramsWithQuery),
-            '/c/test/+/1234/5..10?revert&foo=bar');
-
-        params.messageHash = '#123';
-        assert.equal(element._generateUrl(params), '/c/test/+/1234/5..10#123');
-      });
-
-      test('change with repo name encoding', () => {
-        const params = {
-          view: Gerrit.Nav.View.CHANGE,
-          changeNum: '1234',
-          project: 'x+/y+/z+/w',
-        };
-        assert.equal(element._generateUrl(params),
-            '/c/x%252B/y%252B/z%252B/w/+/1234');
-      });
-
-      test('diff', () => {
-        const params = {
-          view: Gerrit.Nav.View.DIFF,
-          changeNum: '42',
-          path: 'x+y/path.cpp',
-          patchNum: 12,
-        };
-        assert.equal(element._generateUrl(params),
-            '/c/42/12/x%252By/path.cpp');
-
-        params.project = 'test';
-        assert.equal(element._generateUrl(params),
-            '/c/test/+/42/12/x%252By/path.cpp');
-
-        params.basePatchNum = 6;
-        assert.equal(element._generateUrl(params),
-            '/c/test/+/42/6..12/x%252By/path.cpp');
-
-        params.path = 'foo bar/my+file.txt%';
-        params.patchNum = 2;
-        delete params.basePatchNum;
-        assert.equal(element._generateUrl(params),
-            '/c/test/+/42/2/foo+bar/my%252Bfile.txt%2525');
-
-        params.path = 'file.cpp';
-        params.lineNum = 123;
-        assert.equal(element._generateUrl(params),
-            '/c/test/+/42/2/file.cpp#123');
-
-        params.leftSide = true;
-        assert.equal(element._generateUrl(params),
-            '/c/test/+/42/2/file.cpp#b123');
-      });
-
-      test('diff with repo name encoding', () => {
-        const params = {
-          view: Gerrit.Nav.View.DIFF,
-          changeNum: '42',
-          path: 'x+y/path.cpp',
-          patchNum: 12,
-          project: 'x+/y',
-        };
-        assert.equal(element._generateUrl(params),
-            '/c/x%252B/y/+/42/12/x%252By/path.cpp');
-      });
-
-      test('edit', () => {
-        const params = {
-          view: Gerrit.Nav.View.EDIT,
-          changeNum: '42',
-          project: 'test',
-          path: 'x+y/path.cpp',
-        };
-        assert.equal(element._generateUrl(params),
-            '/c/test/+/42/x%252By/path.cpp,edit');
-      });
-
-      test('_getPatchRangeExpression', () => {
-        const params = {};
-        let actual = element._getPatchRangeExpression(params);
-        assert.equal(actual, '');
-
-        params.patchNum = 4;
-        actual = element._getPatchRangeExpression(params);
-        assert.equal(actual, '4');
-
-        params.basePatchNum = 2;
-        actual = element._getPatchRangeExpression(params);
-        assert.equal(actual, '2..4');
-
-        delete params.patchNum;
-        actual = element._getPatchRangeExpression(params);
-        assert.equal(actual, '2..');
-      });
-
-      suite('dashboard', () => {
-        test('self dashboard', () => {
-          const params = {
-            view: Gerrit.Nav.View.DASHBOARD,
+      suite('GWT hash-path URLs', () => {
+        test('redirects hash-path URLs', () => {
+          const data = {
+            canonicalPath: '/#/foo/bar/baz',
+            hash: '/foo/bar/baz',
+            querystring: '',
           };
-          assert.equal(element._generateUrl(params), '/dashboard/self');
-        });
-
-        test('user dashboard', () => {
-          const params = {
-            view: Gerrit.Nav.View.DASHBOARD,
-            user: 'user',
-          };
-          assert.equal(element._generateUrl(params), '/dashboard/user');
-        });
-
-        test('custom self dashboard, no title', () => {
-          const params = {
-            view: Gerrit.Nav.View.DASHBOARD,
-            sections: [
-              {name: 'section 1', query: 'query 1'},
-              {name: 'section 2', query: 'query 2'},
-            ],
-          };
-          assert.equal(
-              element._generateUrl(params),
-              '/dashboard/?section%201=query%201&section%202=query%202');
-        });
-
-        test('custom repo dashboard', () => {
-          const params = {
-            view: Gerrit.Nav.View.DASHBOARD,
-            sections: [
-              {name: 'section 1', query: 'query 1 ${project}'},
-              {name: 'section 2', query: 'query 2 ${repo}'},
-            ],
-            repo: 'repo-name',
-          };
-          assert.equal(
-              element._generateUrl(params),
-              '/dashboard/?section%201=query%201%20repo-name&' +
-              'section%202=query%202%20repo-name');
-        });
-
-        test('custom user dashboard, with title', () => {
-          const params = {
-            view: Gerrit.Nav.View.DASHBOARD,
-            user: 'user',
-            sections: [{name: 'name', query: 'query'}],
-            title: 'custom dashboard',
-          };
-          assert.equal(
-              element._generateUrl(params),
-              '/dashboard/user?name=query&title=custom%20dashboard');
-        });
-
-        test('repo dashboard', () => {
-          const params = {
-            view: Gerrit.Nav.View.DASHBOARD,
-            repo: 'gerrit/repo',
-            dashboard: 'default:main',
-          };
-          assert.equal(
-              element._generateUrl(params),
-              '/p/gerrit/repo/+/dashboard/default:main');
-        });
-
-        test('project dashboard (legacy)', () => {
-          const params = {
-            view: Gerrit.Nav.View.DASHBOARD,
-            project: 'gerrit/project',
-            dashboard: 'default:main',
-          };
-          assert.equal(
-              element._generateUrl(params),
-              '/p/gerrit/project/+/dashboard/default:main');
-        });
-      });
-
-      suite('groups', () => {
-        test('group info', () => {
-          const params = {
-            view: Gerrit.Nav.View.GROUP,
-            groupId: 1234,
-          };
-          assert.equal(element._generateUrl(params), '/admin/groups/1234');
-        });
-
-        test('group members', () => {
-          const params = {
-            view: Gerrit.Nav.View.GROUP,
-            groupId: 1234,
-            detail: 'members',
-          };
-          assert.equal(element._generateUrl(params),
-              '/admin/groups/1234,members');
-        });
-
-        test('group audit log', () => {
-          const params = {
-            view: Gerrit.Nav.View.GROUP,
-            groupId: 1234,
-            detail: 'log',
-          };
-          assert.equal(element._generateUrl(params),
-              '/admin/groups/1234,audit-log');
-        });
-      });
-    });
-
-    suite('param normalization', () => {
-      let projectLookupStub;
-
-      setup(() => {
-        projectLookupStub = sandbox
-            .stub(element.$.restAPI, 'getFromProjectLookup');
-        sandbox.stub(element, '_generateUrl');
-      });
-
-      suite('_normalizeLegacyRouteParams', () => {
-        let rangeStub;
-        let redirectStub;
-        let show404Stub;
-
-        setup(() => {
-          rangeStub = sandbox.stub(element, '_normalizePatchRangeParams')
-              .returns(Promise.resolve());
-          redirectStub = sandbox.stub(element, '_redirect');
-          show404Stub = sandbox.stub(element, '_show404');
-        });
-
-        test('w/o changeNum', () => {
-          projectLookupStub.returns(Promise.resolve('foo/bar'));
-          const params = {};
-          return element._normalizeLegacyRouteParams(params).then(() => {
-            assert.isFalse(projectLookupStub.called);
-            assert.isFalse(rangeStub.called);
-            assert.isNotOk(params.project);
-            assert.isFalse(redirectStub.called);
-            assert.isFalse(show404Stub.called);
-          });
-        });
-
-        test('w/ changeNum', () => {
-          projectLookupStub.returns(Promise.resolve('foo/bar'));
-          const params = {changeNum: 1234};
-          return element._normalizeLegacyRouteParams(params).then(() => {
-            assert.isTrue(projectLookupStub.called);
-            assert.isTrue(rangeStub.called);
-            assert.equal(params.project, 'foo/bar');
-            assert.isTrue(redirectStub.calledOnce);
-            assert.isFalse(show404Stub.called);
-          });
-        });
-
-        test('halts on project lookup failure', () => {
-          projectLookupStub.returns(Promise.resolve(undefined));
-          const params = {changeNum: 1234};
-          return element._normalizeLegacyRouteParams(params).then(() => {
-            assert.isTrue(projectLookupStub.called);
-            assert.isFalse(rangeStub.called);
-            assert.isUndefined(params.project);
-            assert.isFalse(redirectStub.called);
-            assert.isTrue(show404Stub.calledOnce);
-          });
-        });
-      });
-
-      suite('_normalizePatchRangeParams', () => {
-        test('range n..n normalizes to n', () => {
-          const params = {basePatchNum: 4, patchNum: 4};
-          const needsRedirect = element._normalizePatchRangeParams(params);
-          assert.isTrue(needsRedirect);
-          assert.isNotOk(params.basePatchNum);
-          assert.equal(params.patchNum, 4);
-        });
-
-        test('range n.. normalizes to n', () => {
-          const params = {basePatchNum: 4};
-          const needsRedirect = element._normalizePatchRangeParams(params);
-          assert.isFalse(needsRedirect);
-          assert.isNotOk(params.basePatchNum);
-          assert.equal(params.patchNum, 4);
-        });
-      });
-    });
-
-    suite('route handlers', () => {
-      let redirectStub;
-      let setParamsStub;
-      let handlePassThroughRoute;
-
-      // Simple route handlers are direct mappings from parsed route data to a
-      // new set of app.params. This test helper asserts that passing `data`
-      // into `methodName` results in setting the params specified in `params`.
-      function assertDataToParams(data, methodName, params) {
-        element[methodName](data);
-        assert.deepEqual(setParamsStub.lastCall.args[0], params);
-      }
-
-      setup(() => {
-        redirectStub = sandbox.stub(element, '_redirect');
-        setParamsStub = sandbox.stub(element, '_setParams');
-        handlePassThroughRoute = sandbox.stub(element, '_handlePassThroughRoute');
-      });
-
-      test('_handleAgreementsRoute', () => {
-        const data = {params: {}};
-        element._handleAgreementsRoute(data);
-        assert.isTrue(redirectStub.calledOnce);
-        assert.equal(redirectStub.lastCall.args[0], '/settings/#Agreements');
-      });
-
-      test('_handleNewAgreementsRoute', () => {
-        element._handleNewAgreementsRoute({params: {}});
-        assert.isTrue(setParamsStub.calledOnce);
-        assert.equal(setParamsStub.lastCall.args[0].view,
-            Gerrit.Nav.View.AGREEMENTS);
-      });
-
-      test('_handleSettingsLegacyRoute', () => {
-        const data = {params: {0: 'my-token'}};
-        assertDataToParams(data, '_handleSettingsLegacyRoute', {
-          view: Gerrit.Nav.View.SETTINGS,
-          emailToken: 'my-token',
-        });
-      });
-
-      test('_handleSettingsLegacyRoute with +', () => {
-        const data = {params: {0: 'my-token test'}};
-        assertDataToParams(data, '_handleSettingsLegacyRoute', {
-          view: Gerrit.Nav.View.SETTINGS,
-          emailToken: 'my-token+test',
-        });
-      });
-
-      test('_handleSettingsRoute', () => {
-        const data = {};
-        assertDataToParams(data, '_handleSettingsRoute', {
-          view: Gerrit.Nav.View.SETTINGS,
-        });
-      });
-
-      test('_handleDefaultRoute on first load', () => {
-        const appElementStub = {dispatchEvent: sinon.stub()};
-        element._appElement = () => appElementStub;
-        element._handleDefaultRoute();
-        assert.isTrue(appElementStub.dispatchEvent.calledOnce);
-        assert.equal(
-            appElementStub.dispatchEvent.lastCall.args[0].detail.response.status,
-            404);
-      });
-
-      test('_handleDefaultRoute after internal navigation', () => {
-        let onExit = null;
-        const onRegisteringExit = (match, _onExit) => {
-          onExit = _onExit;
-        };
-        sandbox.stub(window.page, 'exit', onRegisteringExit);
-        sandbox.stub(Gerrit.Nav, 'setup');
-        sandbox.stub(window.page, 'start');
-        sandbox.stub(window.page, 'base');
-        sandbox.stub(window, 'page');
-        element._startRouter();
-
-        const appElementStub = {dispatchEvent: sinon.stub()};
-        element._appElement = () => appElementStub;
-        element._handleDefaultRoute();
-
-        onExit('', () => {}); // we left page;
-
-        element._handleDefaultRoute();
-        assert.isTrue(handlePassThroughRoute.calledOnce);
-      });
-
-      test('_handleImproperlyEncodedPlusRoute', () => {
-        // Regression test for Issue 7100.
-        element._handleImproperlyEncodedPlusRoute(
-            {canonicalPath: '/c/test/%20/42', params: ['test', '42']});
-        assert.isTrue(redirectStub.calledOnce);
-        assert.equal(
-            redirectStub.lastCall.args[0],
-            '/c/test/+/42');
-
-        sandbox.stub(element, '_getHashFromCanonicalPath').returns('foo');
-        element._handleImproperlyEncodedPlusRoute(
-            {canonicalPath: '/c/test/%20/42', params: ['test', '42']});
-        assert.equal(
-            redirectStub.lastCall.args[0],
-            '/c/test/+/42#foo');
-      });
-
-      test('_handleQueryRoute', () => {
-        const data = {params: ['project:foo/bar/baz']};
-        assertDataToParams(data, '_handleQueryRoute', {
-          view: Gerrit.Nav.View.SEARCH,
-          query: 'project:foo/bar/baz',
-          offset: undefined,
-        });
-
-        data.params.push(',123', '123');
-        assertDataToParams(data, '_handleQueryRoute', {
-          view: Gerrit.Nav.View.SEARCH,
-          query: 'project:foo/bar/baz',
-          offset: '123',
-        });
-      });
-
-      test('_handleQueryLegacySuffixRoute', () => {
-        element._handleQueryLegacySuffixRoute({path: '/q/foo+bar,n,z'});
-        assert.isTrue(redirectStub.calledOnce);
-        assert.equal(redirectStub.lastCall.args[0], '/q/foo+bar');
-      });
-
-      test('_handleQueryRoute', () => {
-        const data = {params: ['project:foo/bar/baz']};
-        assertDataToParams(data, '_handleQueryRoute', {
-          view: Gerrit.Nav.View.SEARCH,
-          query: 'project:foo/bar/baz',
-          offset: undefined,
-        });
-
-        data.params.push(',123', '123');
-        assertDataToParams(data, '_handleQueryRoute', {
-          view: Gerrit.Nav.View.SEARCH,
-          query: 'project:foo/bar/baz',
-          offset: '123',
-        });
-      });
-
-      suite('_handleRegisterRoute', () => {
-        test('happy path', () => {
-          const ctx = {params: ['/foo/bar']};
-          element._handleRegisterRoute(ctx);
-          assert.isTrue(redirectStub.calledWithExactly('/foo/bar'));
-          assert.isTrue(setParamsStub.calledOnce);
-          assert.isTrue(setParamsStub.lastCall.args[0].justRegistered);
-        });
-
-        test('no param', () => {
-          const ctx = {params: ['']};
-          element._handleRegisterRoute(ctx);
-          assert.isTrue(redirectStub.calledWithExactly('/'));
-          assert.isTrue(setParamsStub.calledOnce);
-          assert.isTrue(setParamsStub.lastCall.args[0].justRegistered);
-        });
-
-        test('prevent redirect', () => {
-          const ctx = {params: ['/register']};
-          element._handleRegisterRoute(ctx);
-          assert.isTrue(redirectStub.calledWithExactly('/'));
-          assert.isTrue(setParamsStub.calledOnce);
-          assert.isTrue(setParamsStub.lastCall.args[0].justRegistered);
-        });
-      });
-
-      suite('_handleRootRoute', () => {
-        test('closes for closeAfterLogin', () => {
-          const data = {querystring: 'closeAfterLogin', canonicalPath: ''};
-          const closeStub = sandbox.stub(window, 'close');
           const result = element._handleRootRoute(data);
           assert.isNotOk(result);
-          assert.isTrue(closeStub.called);
+          assert.isTrue(redirectStub.called);
+          assert.isTrue(redirectStub.calledWithExactly('/foo/bar/baz'));
+        });
+
+        test('redirects hash-path URLs w/o leading slash', () => {
+          const data = {
+            canonicalPath: '/#foo/bar/baz',
+            querystring: '',
+            hash: 'foo/bar/baz',
+          };
+          const result = element._handleRootRoute(data);
+          assert.isNotOk(result);
+          assert.isTrue(redirectStub.called);
+          assert.isTrue(redirectStub.calledWithExactly('/foo/bar/baz'));
+        });
+
+        test('normalizes "/ /" in hash to "/+/"', () => {
+          const data = {
+            canonicalPath: '/#/foo/bar/+/123/4',
+            querystring: '',
+            hash: '/foo/bar/ /123/4',
+          };
+          const result = element._handleRootRoute(data);
+          assert.isNotOk(result);
+          assert.isTrue(redirectStub.called);
+          assert.isTrue(redirectStub.calledWithExactly('/foo/bar/+/123/4'));
+        });
+
+        test('prepends baseurl to hash-path', () => {
+          const data = {
+            canonicalPath: '/#/foo/bar',
+            querystring: '',
+            hash: '/foo/bar',
+          };
+          sandbox.stub(element, 'getBaseUrl').returns('/baz');
+          const result = element._handleRootRoute(data);
+          assert.isNotOk(result);
+          assert.isTrue(redirectStub.called);
+          assert.isTrue(redirectStub.calledWithExactly('/baz/foo/bar'));
+        });
+
+        test('normalizes /VE/ settings hash-paths', () => {
+          const data = {
+            canonicalPath: '/#/VE/foo/bar',
+            querystring: '',
+            hash: '/VE/foo/bar',
+          };
+          const result = element._handleRootRoute(data);
+          assert.isNotOk(result);
+          assert.isTrue(redirectStub.called);
+          assert.isTrue(redirectStub.calledWithExactly(
+              '/settings/VE/foo/bar'));
+        });
+
+        test('does not drop "inner hashes"', () => {
+          const data = {
+            canonicalPath: '/#/foo/bar#baz',
+            querystring: '',
+            hash: '/foo/bar',
+          };
+          const result = element._handleRootRoute(data);
+          assert.isNotOk(result);
+          assert.isTrue(redirectStub.called);
+          assert.isTrue(redirectStub.calledWithExactly('/foo/bar#baz'));
+        });
+      });
+    });
+
+    suite('_handleDashboardRoute', () => {
+      let redirectToLoginStub;
+
+      setup(() => {
+        redirectToLoginStub = sandbox.stub(element, '_redirectToLogin');
+      });
+
+      test('own dashboard but signed out redirects to login', () => {
+        sandbox.stub(element.$.restAPI, 'getLoggedIn')
+            .returns(Promise.resolve(false));
+        const data = {canonicalPath: '/dashboard/', params: {0: 'seLF'}};
+        return element._handleDashboardRoute(data, '').then(() => {
+          assert.isTrue(redirectToLoginStub.calledOnce);
           assert.isFalse(redirectStub.called);
-        });
-
-        test('redirects to dashboard if logged in', () => {
-          sandbox.stub(element.$.restAPI, 'getLoggedIn')
-              .returns(Promise.resolve(true));
-          const data = {
-            canonicalPath: '/', path: '/', querystring: '', hash: '',
-          };
-          const result = element._handleRootRoute(data);
-          assert.isOk(result);
-          return result.then(() => {
-            assert.isTrue(redirectStub.calledWithExactly('/dashboard/self'));
-          });
-        });
-
-        test('redirects to open changes if not logged in', () => {
-          sandbox.stub(element.$.restAPI, 'getLoggedIn')
-              .returns(Promise.resolve(false));
-          const data = {
-            canonicalPath: '/', path: '/', querystring: '', hash: '',
-          };
-          const result = element._handleRootRoute(data);
-          assert.isOk(result);
-          return result.then(() => {
-            assert.isTrue(redirectStub.calledWithExactly('/q/status:open'));
-          });
-        });
-
-        suite('GWT hash-path URLs', () => {
-          test('redirects hash-path URLs', () => {
-            const data = {
-              canonicalPath: '/#/foo/bar/baz',
-              hash: '/foo/bar/baz',
-              querystring: '',
-            };
-            const result = element._handleRootRoute(data);
-            assert.isNotOk(result);
-            assert.isTrue(redirectStub.called);
-            assert.isTrue(redirectStub.calledWithExactly('/foo/bar/baz'));
-          });
-
-          test('redirects hash-path URLs w/o leading slash', () => {
-            const data = {
-              canonicalPath: '/#foo/bar/baz',
-              querystring: '',
-              hash: 'foo/bar/baz',
-            };
-            const result = element._handleRootRoute(data);
-            assert.isNotOk(result);
-            assert.isTrue(redirectStub.called);
-            assert.isTrue(redirectStub.calledWithExactly('/foo/bar/baz'));
-          });
-
-          test('normalizes "/ /" in hash to "/+/"', () => {
-            const data = {
-              canonicalPath: '/#/foo/bar/+/123/4',
-              querystring: '',
-              hash: '/foo/bar/ /123/4',
-            };
-            const result = element._handleRootRoute(data);
-            assert.isNotOk(result);
-            assert.isTrue(redirectStub.called);
-            assert.isTrue(redirectStub.calledWithExactly('/foo/bar/+/123/4'));
-          });
-
-          test('prepends baseurl to hash-path', () => {
-            const data = {
-              canonicalPath: '/#/foo/bar',
-              querystring: '',
-              hash: '/foo/bar',
-            };
-            sandbox.stub(element, 'getBaseUrl').returns('/baz');
-            const result = element._handleRootRoute(data);
-            assert.isNotOk(result);
-            assert.isTrue(redirectStub.called);
-            assert.isTrue(redirectStub.calledWithExactly('/baz/foo/bar'));
-          });
-
-          test('normalizes /VE/ settings hash-paths', () => {
-            const data = {
-              canonicalPath: '/#/VE/foo/bar',
-              querystring: '',
-              hash: '/VE/foo/bar',
-            };
-            const result = element._handleRootRoute(data);
-            assert.isNotOk(result);
-            assert.isTrue(redirectStub.called);
-            assert.isTrue(redirectStub.calledWithExactly(
-                '/settings/VE/foo/bar'));
-          });
-
-          test('does not drop "inner hashes"', () => {
-            const data = {
-              canonicalPath: '/#/foo/bar#baz',
-              querystring: '',
-              hash: '/foo/bar',
-            };
-            const result = element._handleRootRoute(data);
-            assert.isNotOk(result);
-            assert.isTrue(redirectStub.called);
-            assert.isTrue(redirectStub.calledWithExactly('/foo/bar#baz'));
-          });
+          assert.isFalse(setParamsStub.called);
         });
       });
 
-      suite('_handleDashboardRoute', () => {
-        let redirectToLoginStub;
-
-        setup(() => {
-          redirectToLoginStub = sandbox.stub(element, '_redirectToLogin');
-        });
-
-        test('own dashboard but signed out redirects to login', () => {
-          sandbox.stub(element.$.restAPI, 'getLoggedIn')
-              .returns(Promise.resolve(false));
-          const data = {canonicalPath: '/dashboard/', params: {0: 'seLF'}};
-          return element._handleDashboardRoute(data, '').then(() => {
-            assert.isTrue(redirectToLoginStub.calledOnce);
-            assert.isFalse(redirectStub.called);
-            assert.isFalse(setParamsStub.called);
-          });
-        });
-
-        test('non-self dashboard but signed out does not redirect', () => {
-          sandbox.stub(element.$.restAPI, 'getLoggedIn')
-              .returns(Promise.resolve(false));
-          const data = {canonicalPath: '/dashboard/', params: {0: 'foo'}};
-          return element._handleDashboardRoute(data, '').then(() => {
-            assert.isFalse(redirectToLoginStub.called);
-            assert.isFalse(setParamsStub.called);
-            assert.isTrue(redirectStub.calledOnce);
-            assert.equal(redirectStub.lastCall.args[0], '/q/owner:foo');
-          });
-        });
-
-        test('dashboard while signed in sets params', () => {
-          sandbox.stub(element.$.restAPI, 'getLoggedIn')
-              .returns(Promise.resolve(true));
-          const data = {canonicalPath: '/dashboard/', params: {0: 'foo'}};
-          return element._handleDashboardRoute(data, '').then(() => {
-            assert.isFalse(redirectToLoginStub.called);
-            assert.isFalse(redirectStub.called);
-            assert.isTrue(setParamsStub.calledOnce);
-            assert.deepEqual(setParamsStub.lastCall.args[0], {
-              view: Gerrit.Nav.View.DASHBOARD,
-              user: 'foo',
-            });
-          });
-        });
-      });
-
-      suite('_handleCustomDashboardRoute', () => {
-        let redirectToLoginStub;
-
-        setup(() => {
-          redirectToLoginStub = sandbox.stub(element, '_redirectToLogin');
-        });
-
-        test('no user specified', () => {
-          const data = {canonicalPath: '/dashboard/', params: {0: ''}};
-          return element._handleCustomDashboardRoute(data, '').then(() => {
-            assert.isFalse(setParamsStub.called);
-            assert.isTrue(redirectStub.called);
-            assert.equal(redirectStub.lastCall.args[0], '/dashboard/self');
-          });
-        });
-
-        test('custom dashboard without title', () => {
-          const data = {canonicalPath: '/dashboard/', params: {0: ''}};
-          return element._handleCustomDashboardRoute(data, '?a=b&c&d=e')
-              .then(() => {
-                assert.isFalse(redirectStub.called);
-                assert.isTrue(setParamsStub.calledOnce);
-                assert.deepEqual(setParamsStub.lastCall.args[0], {
-                  view: Gerrit.Nav.View.DASHBOARD,
-                  user: 'self',
-                  sections: [
-                    {name: 'a', query: 'b'},
-                    {name: 'd', query: 'e'},
-                  ],
-                  title: 'Custom Dashboard',
-                });
-              });
-        });
-
-        test('custom dashboard with title', () => {
-          const data = {canonicalPath: '/dashboard/', params: {0: ''}};
-          return element._handleCustomDashboardRoute(data,
-              '?a=b&c&d=&=e&title=t')
-              .then(() => {
-                assert.isFalse(redirectToLoginStub.called);
-                assert.isFalse(redirectStub.called);
-                assert.isTrue(setParamsStub.calledOnce);
-                assert.deepEqual(setParamsStub.lastCall.args[0], {
-                  view: Gerrit.Nav.View.DASHBOARD,
-                  user: 'self',
-                  sections: [
-                    {name: 'a', query: 'b'},
-                  ],
-                  title: 't',
-                });
-              });
-        });
-
-        test('custom dashboard with foreach', () => {
-          const data = {canonicalPath: '/dashboard/', params: {0: ''}};
-          return element._handleCustomDashboardRoute(data,
-              '?a=b&c&d=&=e&foreach=is:open')
-              .then(() => {
-                assert.isFalse(redirectToLoginStub.called);
-                assert.isFalse(redirectStub.called);
-                assert.isTrue(setParamsStub.calledOnce);
-                assert.deepEqual(setParamsStub.lastCall.args[0], {
-                  view: Gerrit.Nav.View.DASHBOARD,
-                  user: 'self',
-                  sections: [
-                    {name: 'a', query: 'is:open b'},
-                  ],
-                  title: 'Custom Dashboard',
-                });
-              });
-        });
-      });
-
-      suite('group routes', () => {
-        test('_handleGroupInfoRoute', () => {
-          const data = {params: {0: 1234}};
-          element._handleGroupInfoRoute(data);
+      test('non-self dashboard but signed out does not redirect', () => {
+        sandbox.stub(element.$.restAPI, 'getLoggedIn')
+            .returns(Promise.resolve(false));
+        const data = {canonicalPath: '/dashboard/', params: {0: 'foo'}};
+        return element._handleDashboardRoute(data, '').then(() => {
+          assert.isFalse(redirectToLoginStub.called);
+          assert.isFalse(setParamsStub.called);
           assert.isTrue(redirectStub.calledOnce);
-          assert.equal(redirectStub.lastCall.args[0], '/admin/groups/1234');
+          assert.equal(redirectStub.lastCall.args[0], '/q/owner:foo');
+        });
+      });
+
+      test('dashboard while signed in sets params', () => {
+        sandbox.stub(element.$.restAPI, 'getLoggedIn')
+            .returns(Promise.resolve(true));
+        const data = {canonicalPath: '/dashboard/', params: {0: 'foo'}};
+        return element._handleDashboardRoute(data, '').then(() => {
+          assert.isFalse(redirectToLoginStub.called);
+          assert.isFalse(redirectStub.called);
+          assert.isTrue(setParamsStub.calledOnce);
+          assert.deepEqual(setParamsStub.lastCall.args[0], {
+            view: Gerrit.Nav.View.DASHBOARD,
+            user: 'foo',
+          });
+        });
+      });
+    });
+
+    suite('_handleCustomDashboardRoute', () => {
+      let redirectToLoginStub;
+
+      setup(() => {
+        redirectToLoginStub = sandbox.stub(element, '_redirectToLogin');
+      });
+
+      test('no user specified', () => {
+        const data = {canonicalPath: '/dashboard/', params: {0: ''}};
+        return element._handleCustomDashboardRoute(data, '').then(() => {
+          assert.isFalse(setParamsStub.called);
+          assert.isTrue(redirectStub.called);
+          assert.equal(redirectStub.lastCall.args[0], '/dashboard/self');
+        });
+      });
+
+      test('custom dashboard without title', () => {
+        const data = {canonicalPath: '/dashboard/', params: {0: ''}};
+        return element._handleCustomDashboardRoute(data, '?a=b&c&d=e')
+            .then(() => {
+              assert.isFalse(redirectStub.called);
+              assert.isTrue(setParamsStub.calledOnce);
+              assert.deepEqual(setParamsStub.lastCall.args[0], {
+                view: Gerrit.Nav.View.DASHBOARD,
+                user: 'self',
+                sections: [
+                  {name: 'a', query: 'b'},
+                  {name: 'd', query: 'e'},
+                ],
+                title: 'Custom Dashboard',
+              });
+            });
+      });
+
+      test('custom dashboard with title', () => {
+        const data = {canonicalPath: '/dashboard/', params: {0: ''}};
+        return element._handleCustomDashboardRoute(data,
+            '?a=b&c&d=&=e&title=t')
+            .then(() => {
+              assert.isFalse(redirectToLoginStub.called);
+              assert.isFalse(redirectStub.called);
+              assert.isTrue(setParamsStub.calledOnce);
+              assert.deepEqual(setParamsStub.lastCall.args[0], {
+                view: Gerrit.Nav.View.DASHBOARD,
+                user: 'self',
+                sections: [
+                  {name: 'a', query: 'b'},
+                ],
+                title: 't',
+              });
+            });
+      });
+
+      test('custom dashboard with foreach', () => {
+        const data = {canonicalPath: '/dashboard/', params: {0: ''}};
+        return element._handleCustomDashboardRoute(data,
+            '?a=b&c&d=&=e&foreach=is:open')
+            .then(() => {
+              assert.isFalse(redirectToLoginStub.called);
+              assert.isFalse(redirectStub.called);
+              assert.isTrue(setParamsStub.calledOnce);
+              assert.deepEqual(setParamsStub.lastCall.args[0], {
+                view: Gerrit.Nav.View.DASHBOARD,
+                user: 'self',
+                sections: [
+                  {name: 'a', query: 'is:open b'},
+                ],
+                title: 'Custom Dashboard',
+              });
+            });
+      });
+    });
+
+    suite('group routes', () => {
+      test('_handleGroupInfoRoute', () => {
+        const data = {params: {0: 1234}};
+        element._handleGroupInfoRoute(data);
+        assert.isTrue(redirectStub.calledOnce);
+        assert.equal(redirectStub.lastCall.args[0], '/admin/groups/1234');
+      });
+
+      test('_handleGroupAuditLogRoute', () => {
+        const data = {params: {0: 1234}};
+        assertDataToParams(data, '_handleGroupAuditLogRoute', {
+          view: Gerrit.Nav.View.GROUP,
+          detail: 'log',
+          groupId: 1234,
+        });
+      });
+
+      test('_handleGroupMembersRoute', () => {
+        const data = {params: {0: 1234}};
+        assertDataToParams(data, '_handleGroupMembersRoute', {
+          view: Gerrit.Nav.View.GROUP,
+          detail: 'members',
+          groupId: 1234,
+        });
+      });
+
+      test('_handleGroupListOffsetRoute', () => {
+        const data = {params: {}};
+        assertDataToParams(data, '_handleGroupListOffsetRoute', {
+          view: Gerrit.Nav.View.ADMIN,
+          adminView: 'gr-admin-group-list',
+          offset: 0,
+          filter: null,
+          openCreateModal: false,
         });
 
-        test('_handleGroupAuditLogRoute', () => {
-          const data = {params: {0: 1234}};
-          assertDataToParams(data, '_handleGroupAuditLogRoute', {
-            view: Gerrit.Nav.View.GROUP,
-            detail: 'log',
-            groupId: 1234,
+        data.params[1] = 42;
+        assertDataToParams(data, '_handleGroupListOffsetRoute', {
+          view: Gerrit.Nav.View.ADMIN,
+          adminView: 'gr-admin-group-list',
+          offset: 42,
+          filter: null,
+          openCreateModal: false,
+        });
+
+        data.hash = 'create';
+        assertDataToParams(data, '_handleGroupListOffsetRoute', {
+          view: Gerrit.Nav.View.ADMIN,
+          adminView: 'gr-admin-group-list',
+          offset: 42,
+          filter: null,
+          openCreateModal: true,
+        });
+      });
+
+      test('_handleGroupListFilterOffsetRoute', () => {
+        const data = {params: {filter: 'foo', offset: 42}};
+        assertDataToParams(data, '_handleGroupListFilterOffsetRoute', {
+          view: Gerrit.Nav.View.ADMIN,
+          adminView: 'gr-admin-group-list',
+          offset: 42,
+          filter: 'foo',
+        });
+      });
+
+      test('_handleGroupListFilterRoute', () => {
+        const data = {params: {filter: 'foo'}};
+        assertDataToParams(data, '_handleGroupListFilterRoute', {
+          view: Gerrit.Nav.View.ADMIN,
+          adminView: 'gr-admin-group-list',
+          filter: 'foo',
+        });
+      });
+
+      test('_handleGroupRoute', () => {
+        const data = {params: {0: 4321}};
+        assertDataToParams(data, '_handleGroupRoute', {
+          view: Gerrit.Nav.View.GROUP,
+          groupId: 4321,
+        });
+      });
+    });
+
+    suite('repo routes', () => {
+      test('_handleProjectsOldRoute', () => {
+        const data = {params: {}};
+        element._handleProjectsOldRoute(data);
+        assert.isTrue(redirectStub.calledOnce);
+        assert.equal(redirectStub.lastCall.args[0], '/admin/repos/');
+      });
+
+      test('_handleProjectsOldRoute test', () => {
+        const data = {params: {1: 'test'}};
+        element._handleProjectsOldRoute(data);
+        assert.isTrue(redirectStub.calledOnce);
+        assert.equal(redirectStub.lastCall.args[0], '/admin/repos/test');
+      });
+
+      test('_handleProjectsOldRoute test,branches', () => {
+        const data = {params: {1: 'test,branches'}};
+        element._handleProjectsOldRoute(data);
+        assert.isTrue(redirectStub.calledOnce);
+        assert.equal(
+            redirectStub.lastCall.args[0], '/admin/repos/test,branches');
+      });
+
+      test('_handleRepoRoute', () => {
+        const data = {params: {0: 4321}};
+        assertDataToParams(data, '_handleRepoRoute', {
+          view: Gerrit.Nav.View.REPO,
+          repo: 4321,
+        });
+      });
+
+      test('_handleRepoCommandsRoute', () => {
+        const data = {params: {0: 4321}};
+        assertDataToParams(data, '_handleRepoCommandsRoute', {
+          view: Gerrit.Nav.View.REPO,
+          detail: Gerrit.Nav.RepoDetailView.COMMANDS,
+          repo: 4321,
+        });
+      });
+
+      test('_handleRepoAccessRoute', () => {
+        const data = {params: {0: 4321}};
+        assertDataToParams(data, '_handleRepoAccessRoute', {
+          view: Gerrit.Nav.View.REPO,
+          detail: Gerrit.Nav.RepoDetailView.ACCESS,
+          repo: 4321,
+        });
+      });
+
+      suite('branch list routes', () => {
+        test('_handleBranchListOffsetRoute', () => {
+          const data = {params: {0: 4321}};
+          assertDataToParams(data, '_handleBranchListOffsetRoute', {
+            view: Gerrit.Nav.View.REPO,
+            detail: Gerrit.Nav.RepoDetailView.BRANCHES,
+            repo: 4321,
+            offset: 0,
+            filter: null,
+          });
+
+          data.params[2] = 42;
+          assertDataToParams(data, '_handleBranchListOffsetRoute', {
+            view: Gerrit.Nav.View.REPO,
+            detail: Gerrit.Nav.RepoDetailView.BRANCHES,
+            repo: 4321,
+            offset: 42,
+            filter: null,
           });
         });
 
-        test('_handleGroupMembersRoute', () => {
-          const data = {params: {0: 1234}};
-          assertDataToParams(data, '_handleGroupMembersRoute', {
-            view: Gerrit.Nav.View.GROUP,
-            detail: 'members',
-            groupId: 1234,
+        test('_handleBranchListFilterOffsetRoute', () => {
+          const data = {params: {repo: 4321, filter: 'foo', offset: 42}};
+          assertDataToParams(data, '_handleBranchListFilterOffsetRoute', {
+            view: Gerrit.Nav.View.REPO,
+            detail: Gerrit.Nav.RepoDetailView.BRANCHES,
+            repo: 4321,
+            offset: 42,
+            filter: 'foo',
           });
         });
 
-        test('_handleGroupListOffsetRoute', () => {
+        test('_handleBranchListFilterRoute', () => {
+          const data = {params: {repo: 4321, filter: 'foo'}};
+          assertDataToParams(data, '_handleBranchListFilterRoute', {
+            view: Gerrit.Nav.View.REPO,
+            detail: Gerrit.Nav.RepoDetailView.BRANCHES,
+            repo: 4321,
+            filter: 'foo',
+          });
+        });
+      });
+
+      suite('tag list routes', () => {
+        test('_handleTagListOffsetRoute', () => {
+          const data = {params: {0: 4321}};
+          assertDataToParams(data, '_handleTagListOffsetRoute', {
+            view: Gerrit.Nav.View.REPO,
+            detail: Gerrit.Nav.RepoDetailView.TAGS,
+            repo: 4321,
+            offset: 0,
+            filter: null,
+          });
+        });
+
+        test('_handleTagListFilterOffsetRoute', () => {
+          const data = {params: {repo: 4321, filter: 'foo', offset: 42}};
+          assertDataToParams(data, '_handleTagListFilterOffsetRoute', {
+            view: Gerrit.Nav.View.REPO,
+            detail: Gerrit.Nav.RepoDetailView.TAGS,
+            repo: 4321,
+            offset: 42,
+            filter: 'foo',
+          });
+        });
+
+        test('_handleTagListFilterRoute', () => {
+          const data = {params: {repo: 4321}};
+          assertDataToParams(data, '_handleTagListFilterRoute', {
+            view: Gerrit.Nav.View.REPO,
+            detail: Gerrit.Nav.RepoDetailView.TAGS,
+            repo: 4321,
+            filter: null,
+          });
+
+          data.params.filter = 'foo';
+          assertDataToParams(data, '_handleTagListFilterRoute', {
+            view: Gerrit.Nav.View.REPO,
+            detail: Gerrit.Nav.RepoDetailView.TAGS,
+            repo: 4321,
+            filter: 'foo',
+          });
+        });
+      });
+
+      suite('repo list routes', () => {
+        test('_handleRepoListOffsetRoute', () => {
           const data = {params: {}};
-          assertDataToParams(data, '_handleGroupListOffsetRoute', {
+          assertDataToParams(data, '_handleRepoListOffsetRoute', {
             view: Gerrit.Nav.View.ADMIN,
-            adminView: 'gr-admin-group-list',
+            adminView: 'gr-repo-list',
             offset: 0,
             filter: null,
             openCreateModal: false,
           });
 
           data.params[1] = 42;
-          assertDataToParams(data, '_handleGroupListOffsetRoute', {
+          assertDataToParams(data, '_handleRepoListOffsetRoute', {
             view: Gerrit.Nav.View.ADMIN,
-            adminView: 'gr-admin-group-list',
+            adminView: 'gr-repo-list',
             offset: 42,
             filter: null,
             openCreateModal: false,
           });
 
           data.hash = 'create';
-          assertDataToParams(data, '_handleGroupListOffsetRoute', {
+          assertDataToParams(data, '_handleRepoListOffsetRoute', {
             view: Gerrit.Nav.View.ADMIN,
-            adminView: 'gr-admin-group-list',
+            adminView: 'gr-repo-list',
             offset: 42,
             filter: null,
             openCreateModal: true,
           });
         });
 
-        test('_handleGroupListFilterOffsetRoute', () => {
+        test('_handleRepoListFilterOffsetRoute', () => {
           const data = {params: {filter: 'foo', offset: 42}};
-          assertDataToParams(data, '_handleGroupListFilterOffsetRoute', {
+          assertDataToParams(data, '_handleRepoListFilterOffsetRoute', {
             view: Gerrit.Nav.View.ADMIN,
-            adminView: 'gr-admin-group-list',
+            adminView: 'gr-repo-list',
             offset: 42,
             filter: 'foo',
           });
         });
 
-        test('_handleGroupListFilterRoute', () => {
-          const data = {params: {filter: 'foo'}};
-          assertDataToParams(data, '_handleGroupListFilterRoute', {
-            view: Gerrit.Nav.View.ADMIN,
-            adminView: 'gr-admin-group-list',
-            filter: 'foo',
-          });
-        });
-
-        test('_handleGroupRoute', () => {
-          const data = {params: {0: 4321}};
-          assertDataToParams(data, '_handleGroupRoute', {
-            view: Gerrit.Nav.View.GROUP,
-            groupId: 4321,
-          });
-        });
-      });
-
-      suite('repo routes', () => {
-        test('_handleProjectsOldRoute', () => {
+        test('_handleRepoListFilterRoute', () => {
           const data = {params: {}};
-          element._handleProjectsOldRoute(data);
-          assert.isTrue(redirectStub.calledOnce);
-          assert.equal(redirectStub.lastCall.args[0], '/admin/repos/');
-        });
-
-        test('_handleProjectsOldRoute test', () => {
-          const data = {params: {1: 'test'}};
-          element._handleProjectsOldRoute(data);
-          assert.isTrue(redirectStub.calledOnce);
-          assert.equal(redirectStub.lastCall.args[0], '/admin/repos/test');
-        });
-
-        test('_handleProjectsOldRoute test,branches', () => {
-          const data = {params: {1: 'test,branches'}};
-          element._handleProjectsOldRoute(data);
-          assert.isTrue(redirectStub.calledOnce);
-          assert.equal(
-              redirectStub.lastCall.args[0], '/admin/repos/test,branches');
-        });
-
-        test('_handleRepoRoute', () => {
-          const data = {params: {0: 4321}};
-          assertDataToParams(data, '_handleRepoRoute', {
-            view: Gerrit.Nav.View.REPO,
-            repo: 4321,
-          });
-        });
-
-        test('_handleRepoCommandsRoute', () => {
-          const data = {params: {0: 4321}};
-          assertDataToParams(data, '_handleRepoCommandsRoute', {
-            view: Gerrit.Nav.View.REPO,
-            detail: Gerrit.Nav.RepoDetailView.COMMANDS,
-            repo: 4321,
-          });
-        });
-
-        test('_handleRepoAccessRoute', () => {
-          const data = {params: {0: 4321}};
-          assertDataToParams(data, '_handleRepoAccessRoute', {
-            view: Gerrit.Nav.View.REPO,
-            detail: Gerrit.Nav.RepoDetailView.ACCESS,
-            repo: 4321,
-          });
-        });
-
-        suite('branch list routes', () => {
-          test('_handleBranchListOffsetRoute', () => {
-            const data = {params: {0: 4321}};
-            assertDataToParams(data, '_handleBranchListOffsetRoute', {
-              view: Gerrit.Nav.View.REPO,
-              detail: Gerrit.Nav.RepoDetailView.BRANCHES,
-              repo: 4321,
-              offset: 0,
-              filter: null,
-            });
-
-            data.params[2] = 42;
-            assertDataToParams(data, '_handleBranchListOffsetRoute', {
-              view: Gerrit.Nav.View.REPO,
-              detail: Gerrit.Nav.RepoDetailView.BRANCHES,
-              repo: 4321,
-              offset: 42,
-              filter: null,
-            });
-          });
-
-          test('_handleBranchListFilterOffsetRoute', () => {
-            const data = {params: {repo: 4321, filter: 'foo', offset: 42}};
-            assertDataToParams(data, '_handleBranchListFilterOffsetRoute', {
-              view: Gerrit.Nav.View.REPO,
-              detail: Gerrit.Nav.RepoDetailView.BRANCHES,
-              repo: 4321,
-              offset: 42,
-              filter: 'foo',
-            });
-          });
-
-          test('_handleBranchListFilterRoute', () => {
-            const data = {params: {repo: 4321, filter: 'foo'}};
-            assertDataToParams(data, '_handleBranchListFilterRoute', {
-              view: Gerrit.Nav.View.REPO,
-              detail: Gerrit.Nav.RepoDetailView.BRANCHES,
-              repo: 4321,
-              filter: 'foo',
-            });
-          });
-        });
-
-        suite('tag list routes', () => {
-          test('_handleTagListOffsetRoute', () => {
-            const data = {params: {0: 4321}};
-            assertDataToParams(data, '_handleTagListOffsetRoute', {
-              view: Gerrit.Nav.View.REPO,
-              detail: Gerrit.Nav.RepoDetailView.TAGS,
-              repo: 4321,
-              offset: 0,
-              filter: null,
-            });
-          });
-
-          test('_handleTagListFilterOffsetRoute', () => {
-            const data = {params: {repo: 4321, filter: 'foo', offset: 42}};
-            assertDataToParams(data, '_handleTagListFilterOffsetRoute', {
-              view: Gerrit.Nav.View.REPO,
-              detail: Gerrit.Nav.RepoDetailView.TAGS,
-              repo: 4321,
-              offset: 42,
-              filter: 'foo',
-            });
-          });
-
-          test('_handleTagListFilterRoute', () => {
-            const data = {params: {repo: 4321}};
-            assertDataToParams(data, '_handleTagListFilterRoute', {
-              view: Gerrit.Nav.View.REPO,
-              detail: Gerrit.Nav.RepoDetailView.TAGS,
-              repo: 4321,
-              filter: null,
-            });
-
-            data.params.filter = 'foo';
-            assertDataToParams(data, '_handleTagListFilterRoute', {
-              view: Gerrit.Nav.View.REPO,
-              detail: Gerrit.Nav.RepoDetailView.TAGS,
-              repo: 4321,
-              filter: 'foo',
-            });
-          });
-        });
-
-        suite('repo list routes', () => {
-          test('_handleRepoListOffsetRoute', () => {
-            const data = {params: {}};
-            assertDataToParams(data, '_handleRepoListOffsetRoute', {
-              view: Gerrit.Nav.View.ADMIN,
-              adminView: 'gr-repo-list',
-              offset: 0,
-              filter: null,
-              openCreateModal: false,
-            });
-
-            data.params[1] = 42;
-            assertDataToParams(data, '_handleRepoListOffsetRoute', {
-              view: Gerrit.Nav.View.ADMIN,
-              adminView: 'gr-repo-list',
-              offset: 42,
-              filter: null,
-              openCreateModal: false,
-            });
-
-            data.hash = 'create';
-            assertDataToParams(data, '_handleRepoListOffsetRoute', {
-              view: Gerrit.Nav.View.ADMIN,
-              adminView: 'gr-repo-list',
-              offset: 42,
-              filter: null,
-              openCreateModal: true,
-            });
-          });
-
-          test('_handleRepoListFilterOffsetRoute', () => {
-            const data = {params: {filter: 'foo', offset: 42}};
-            assertDataToParams(data, '_handleRepoListFilterOffsetRoute', {
-              view: Gerrit.Nav.View.ADMIN,
-              adminView: 'gr-repo-list',
-              offset: 42,
-              filter: 'foo',
-            });
-          });
-
-          test('_handleRepoListFilterRoute', () => {
-            const data = {params: {}};
-            assertDataToParams(data, '_handleRepoListFilterRoute', {
-              view: Gerrit.Nav.View.ADMIN,
-              adminView: 'gr-repo-list',
-              filter: null,
-            });
-
-            data.params.filter = 'foo';
-            assertDataToParams(data, '_handleRepoListFilterRoute', {
-              view: Gerrit.Nav.View.ADMIN,
-              adminView: 'gr-repo-list',
-              filter: 'foo',
-            });
-          });
-        });
-      });
-
-      suite('plugin routes', () => {
-        test('_handlePluginListOffsetRoute', () => {
-          const data = {params: {}};
-          assertDataToParams(data, '_handlePluginListOffsetRoute', {
+          assertDataToParams(data, '_handleRepoListFilterRoute', {
             view: Gerrit.Nav.View.ADMIN,
-            adminView: 'gr-plugin-list',
-            offset: 0,
-            filter: null,
-          });
-
-          data.params[1] = 42;
-          assertDataToParams(data, '_handlePluginListOffsetRoute', {
-            view: Gerrit.Nav.View.ADMIN,
-            adminView: 'gr-plugin-list',
-            offset: 42,
-            filter: null,
-          });
-        });
-
-        test('_handlePluginListFilterOffsetRoute', () => {
-          const data = {params: {filter: 'foo', offset: 42}};
-          assertDataToParams(data, '_handlePluginListFilterOffsetRoute', {
-            view: Gerrit.Nav.View.ADMIN,
-            adminView: 'gr-plugin-list',
-            offset: 42,
-            filter: 'foo',
-          });
-        });
-
-        test('_handlePluginListFilterRoute', () => {
-          const data = {params: {}};
-          assertDataToParams(data, '_handlePluginListFilterRoute', {
-            view: Gerrit.Nav.View.ADMIN,
-            adminView: 'gr-plugin-list',
+            adminView: 'gr-repo-list',
             filter: null,
           });
 
           data.params.filter = 'foo';
-          assertDataToParams(data, '_handlePluginListFilterRoute', {
+          assertDataToParams(data, '_handleRepoListFilterRoute', {
             view: Gerrit.Nav.View.ADMIN,
-            adminView: 'gr-plugin-list',
+            adminView: 'gr-repo-list',
             filter: 'foo',
           });
         });
-
-        test('_handlePluginListRoute', () => {
-          const data = {params: {}};
-          assertDataToParams(data, '_handlePluginListRoute', {
-            view: Gerrit.Nav.View.ADMIN,
-            adminView: 'gr-plugin-list',
-          });
-        });
-      });
-
-      suite('change/diff routes', () => {
-        test('_handleChangeNumberLegacyRoute', () => {
-          const data = {params: {0: 12345}};
-          element._handleChangeNumberLegacyRoute(data);
-          assert.isTrue(redirectStub.calledOnce);
-          assert.isTrue(redirectStub.calledWithExactly('/c/12345'));
-        });
-
-        test('_handleChangeLegacyRoute', () => {
-          const normalizeRouteStub = sandbox.stub(element,
-              '_normalizeLegacyRouteParams');
-          const ctx = {
-            params: [
-              1234, // 0 Change number
-              null, // 1 Unused
-              null, // 2 Unused
-              6, // 3 Base patch number
-              null, // 4 Unused
-              9, // 5 Patch number
-            ],
-            querystring: '',
-          };
-          element._handleChangeLegacyRoute(ctx);
-          assert.isTrue(normalizeRouteStub.calledOnce);
-          assert.deepEqual(normalizeRouteStub.lastCall.args[0], {
-            changeNum: 1234,
-            basePatchNum: 6,
-            patchNum: 9,
-            view: Gerrit.Nav.View.CHANGE,
-            querystring: '',
-          });
-        });
-
-        test('_handleDiffLegacyRoute', () => {
-          const normalizeRouteStub = sandbox.stub(element,
-              '_normalizeLegacyRouteParams');
-          const ctx = {
-            params: [
-              1234, // 0 Change number
-              null, // 1 Unused
-              3, // 2 Base patch number
-              null, // 3 Unused
-              8, // 4 Patch number
-              'foo/bar', // 5 Diff path
-            ],
-            path: '/c/1234/3..8/foo/bar',
-            hash: 'b123',
-          };
-          element._handleDiffLegacyRoute(ctx);
-          assert.isFalse(redirectStub.called);
-          assert.isTrue(normalizeRouteStub.calledOnce);
-          assert.deepEqual(normalizeRouteStub.lastCall.args[0], {
-            changeNum: 1234,
-            basePatchNum: 3,
-            patchNum: 8,
-            view: Gerrit.Nav.View.DIFF,
-            path: 'foo/bar',
-            lineNum: 123,
-            leftSide: true,
-          });
-        });
-
-        test('_handleLegacyLinenum w/ @321', () => {
-          const ctx = {path: '/c/1234/3..8/foo/bar@321'};
-          element._handleLegacyLinenum(ctx);
-          assert.isTrue(redirectStub.calledOnce);
-          assert.isTrue(redirectStub.calledWithExactly(
-              '/c/1234/3..8/foo/bar#321'));
-        });
-
-        test('_handleLegacyLinenum w/ @b123', () => {
-          const ctx = {path: '/c/1234/3..8/foo/bar@b123'};
-          element._handleLegacyLinenum(ctx);
-          assert.isTrue(redirectStub.calledOnce);
-          assert.isTrue(redirectStub.calledWithExactly(
-              '/c/1234/3..8/foo/bar#b123'));
-        });
-
-        suite('_handleChangeRoute', () => {
-          let normalizeRangeStub;
-
-          function makeParams(path, hash) {
-            return {
-              params: [
-                'foo/bar', // 0 Project
-                1234, // 1 Change number
-                null, // 2 Unused
-                null, // 3 Unused
-                4, // 4 Base patch number
-                null, // 5 Unused
-                7, // 6 Patch number
-              ],
-            };
-          }
-
-          setup(() => {
-            normalizeRangeStub = sandbox.stub(element,
-                '_normalizePatchRangeParams');
-            sandbox.stub(element.$.restAPI, 'setInProjectLookup');
-          });
-
-          test('needs redirect', () => {
-            normalizeRangeStub.returns(true);
-            sandbox.stub(element, '_generateUrl').returns('foo');
-            const ctx = makeParams(null, '');
-            element._handleChangeRoute(ctx);
-            assert.isTrue(normalizeRangeStub.called);
-            assert.isFalse(setParamsStub.called);
-            assert.isTrue(redirectStub.calledOnce);
-            assert.isTrue(redirectStub.calledWithExactly('foo'));
-          });
-
-          test('change view', () => {
-            normalizeRangeStub.returns(false);
-            sandbox.stub(element, '_generateUrl').returns('foo');
-            const ctx = makeParams(null, '');
-            assertDataToParams(ctx, '_handleChangeRoute', {
-              view: Gerrit.Nav.View.CHANGE,
-              project: 'foo/bar',
-              changeNum: 1234,
-              basePatchNum: 4,
-              patchNum: 7,
-            });
-            assert.isFalse(redirectStub.called);
-            assert.isTrue(normalizeRangeStub.called);
-          });
-        });
-
-        suite('_handleDiffRoute', () => {
-          let normalizeRangeStub;
-
-          function makeParams(path, hash) {
-            return {
-              params: [
-                'foo/bar', // 0 Project
-                1234, // 1 Change number
-                null, // 2 Unused
-                null, // 3 Unused
-                4, // 4 Base patch number
-                null, // 5 Unused
-                7, // 6 Patch number
-                null, // 7 Unused,
-                path, // 8 Diff path
-              ],
-              hash,
-            };
-          }
-
-          setup(() => {
-            normalizeRangeStub = sandbox.stub(element,
-                '_normalizePatchRangeParams');
-            sandbox.stub(element.$.restAPI, 'setInProjectLookup');
-          });
-
-          test('needs redirect', () => {
-            normalizeRangeStub.returns(true);
-            sandbox.stub(element, '_generateUrl').returns('foo');
-            const ctx = makeParams(null, '');
-            element._handleDiffRoute(ctx);
-            assert.isTrue(normalizeRangeStub.called);
-            assert.isFalse(setParamsStub.called);
-            assert.isTrue(redirectStub.calledOnce);
-            assert.isTrue(redirectStub.calledWithExactly('foo'));
-          });
-
-          test('diff view', () => {
-            normalizeRangeStub.returns(false);
-            sandbox.stub(element, '_generateUrl').returns('foo');
-            const ctx = makeParams('foo/bar/baz', 'b44');
-            assertDataToParams(ctx, '_handleDiffRoute', {
-              view: Gerrit.Nav.View.DIFF,
-              project: 'foo/bar',
-              changeNum: 1234,
-              basePatchNum: 4,
-              patchNum: 7,
-              path: 'foo/bar/baz',
-              leftSide: true,
-              lineNum: 44,
-            });
-            assert.isFalse(redirectStub.called);
-            assert.isTrue(normalizeRangeStub.called);
-          });
-        });
-
-        test('_handleDiffEditRoute', () => {
-          const normalizeRangeSpy =
-              sandbox.spy(element, '_normalizePatchRangeParams');
-          sandbox.stub(element.$.restAPI, 'setInProjectLookup');
-          const ctx = {
-            params: [
-              'foo/bar', // 0 Project
-              1234, // 1 Change number
-              3, // 2 Patch num
-              'foo/bar/baz', // 3 File path
-            ],
-          };
-          const appParams = {
-            project: 'foo/bar',
-            changeNum: 1234,
-            view: Gerrit.Nav.View.EDIT,
-            path: 'foo/bar/baz',
-            patchNum: 3,
-            lineNum: undefined,
-          };
-
-          element._handleDiffEditRoute(ctx);
-          assert.isFalse(redirectStub.called);
-          assert.isTrue(normalizeRangeSpy.calledOnce);
-          assert.deepEqual(normalizeRangeSpy.lastCall.args[0], appParams);
-          assert.isFalse(normalizeRangeSpy.lastCall.returnValue);
-          assert.deepEqual(setParamsStub.lastCall.args[0], appParams);
-        });
-
-        test('_handleDiffEditRoute with lineNum', () => {
-          const normalizeRangeSpy =
-              sandbox.spy(element, '_normalizePatchRangeParams');
-          sandbox.stub(element.$.restAPI, 'setInProjectLookup');
-          const ctx = {
-            params: [
-              'foo/bar', // 0 Project
-              1234, // 1 Change number
-              3, // 2 Patch num
-              'foo/bar/baz', // 3 File path
-            ],
-            hash: 4,
-          };
-          const appParams = {
-            project: 'foo/bar',
-            changeNum: 1234,
-            view: Gerrit.Nav.View.EDIT,
-            path: 'foo/bar/baz',
-            patchNum: 3,
-            lineNum: 4,
-          };
-
-          element._handleDiffEditRoute(ctx);
-          assert.isFalse(redirectStub.called);
-          assert.isTrue(normalizeRangeSpy.calledOnce);
-          assert.deepEqual(normalizeRangeSpy.lastCall.args[0], appParams);
-          assert.isFalse(normalizeRangeSpy.lastCall.returnValue);
-          assert.deepEqual(setParamsStub.lastCall.args[0], appParams);
-        });
-
-        test('_handleChangeEditRoute', () => {
-          const normalizeRangeSpy =
-              sandbox.spy(element, '_normalizePatchRangeParams');
-          sandbox.stub(element.$.restAPI, 'setInProjectLookup');
-          const ctx = {
-            params: [
-              'foo/bar', // 0 Project
-              1234, // 1 Change number
-              null,
-              3, // 3 Patch num
-            ],
-          };
-          const appParams = {
-            project: 'foo/bar',
-            changeNum: 1234,
-            view: Gerrit.Nav.View.CHANGE,
-            patchNum: 3,
-            edit: true,
-          };
-
-          element._handleChangeEditRoute(ctx);
-          assert.isFalse(redirectStub.called);
-          assert.isTrue(normalizeRangeSpy.calledOnce);
-          assert.deepEqual(normalizeRangeSpy.lastCall.args[0], appParams);
-          assert.isFalse(normalizeRangeSpy.lastCall.returnValue);
-          assert.deepEqual(setParamsStub.lastCall.args[0], appParams);
-        });
-      });
-
-      test('_handlePluginScreen', () => {
-        const ctx = {params: ['foo', 'bar']};
-        assertDataToParams(ctx, '_handlePluginScreen', {
-          view: Gerrit.Nav.View.PLUGIN_SCREEN,
-          plugin: 'foo',
-          screen: 'bar',
-        });
-        assert.isFalse(redirectStub.called);
       });
     });
 
-    suite('_parseQueryString', () => {
-      test('empty queries', () => {
-        assert.deepEqual(element._parseQueryString(''), []);
-        assert.deepEqual(element._parseQueryString('?'), []);
-        assert.deepEqual(element._parseQueryString('??'), []);
-        assert.deepEqual(element._parseQueryString('&&&'), []);
+    suite('plugin routes', () => {
+      test('_handlePluginListOffsetRoute', () => {
+        const data = {params: {}};
+        assertDataToParams(data, '_handlePluginListOffsetRoute', {
+          view: Gerrit.Nav.View.ADMIN,
+          adminView: 'gr-plugin-list',
+          offset: 0,
+          filter: null,
+        });
+
+        data.params[1] = 42;
+        assertDataToParams(data, '_handlePluginListOffsetRoute', {
+          view: Gerrit.Nav.View.ADMIN,
+          adminView: 'gr-plugin-list',
+          offset: 42,
+          filter: null,
+        });
       });
 
-      test('url decoding', () => {
-        assert.deepEqual(element._parseQueryString('+'), [[' ', '']]);
-        assert.deepEqual(element._parseQueryString('???+%3d+'), [[' = ', '']]);
-        assert.deepEqual(
-            element._parseQueryString('%6e%61%6d%65=%76%61%6c%75%65'),
-            [['name', 'value']]);
+      test('_handlePluginListFilterOffsetRoute', () => {
+        const data = {params: {filter: 'foo', offset: 42}};
+        assertDataToParams(data, '_handlePluginListFilterOffsetRoute', {
+          view: Gerrit.Nav.View.ADMIN,
+          adminView: 'gr-plugin-list',
+          offset: 42,
+          filter: 'foo',
+        });
       });
 
-      test('multiple parameters', () => {
-        assert.deepEqual(
-            element._parseQueryString('a=b&c=d&e=f'),
-            [['a', 'b'], ['c', 'd'], ['e', 'f']]);
-        assert.deepEqual(
-            element._parseQueryString('&a=b&&&e=f&'),
-            [['a', 'b'], ['e', 'f']]);
+      test('_handlePluginListFilterRoute', () => {
+        const data = {params: {}};
+        assertDataToParams(data, '_handlePluginListFilterRoute', {
+          view: Gerrit.Nav.View.ADMIN,
+          adminView: 'gr-plugin-list',
+          filter: null,
+        });
+
+        data.params.filter = 'foo';
+        assertDataToParams(data, '_handlePluginListFilterRoute', {
+          view: Gerrit.Nav.View.ADMIN,
+          adminView: 'gr-plugin-list',
+          filter: 'foo',
+        });
       });
+
+      test('_handlePluginListRoute', () => {
+        const data = {params: {}};
+        assertDataToParams(data, '_handlePluginListRoute', {
+          view: Gerrit.Nav.View.ADMIN,
+          adminView: 'gr-plugin-list',
+        });
+      });
+    });
+
+    suite('change/diff routes', () => {
+      test('_handleChangeNumberLegacyRoute', () => {
+        const data = {params: {0: 12345}};
+        element._handleChangeNumberLegacyRoute(data);
+        assert.isTrue(redirectStub.calledOnce);
+        assert.isTrue(redirectStub.calledWithExactly('/c/12345'));
+      });
+
+      test('_handleChangeLegacyRoute', () => {
+        const normalizeRouteStub = sandbox.stub(element,
+            '_normalizeLegacyRouteParams');
+        const ctx = {
+          params: [
+            1234, // 0 Change number
+            null, // 1 Unused
+            null, // 2 Unused
+            6, // 3 Base patch number
+            null, // 4 Unused
+            9, // 5 Patch number
+          ],
+          querystring: '',
+        };
+        element._handleChangeLegacyRoute(ctx);
+        assert.isTrue(normalizeRouteStub.calledOnce);
+        assert.deepEqual(normalizeRouteStub.lastCall.args[0], {
+          changeNum: 1234,
+          basePatchNum: 6,
+          patchNum: 9,
+          view: Gerrit.Nav.View.CHANGE,
+          querystring: '',
+        });
+      });
+
+      test('_handleDiffLegacyRoute', () => {
+        const normalizeRouteStub = sandbox.stub(element,
+            '_normalizeLegacyRouteParams');
+        const ctx = {
+          params: [
+            1234, // 0 Change number
+            null, // 1 Unused
+            3, // 2 Base patch number
+            null, // 3 Unused
+            8, // 4 Patch number
+            'foo/bar', // 5 Diff path
+          ],
+          path: '/c/1234/3..8/foo/bar',
+          hash: 'b123',
+        };
+        element._handleDiffLegacyRoute(ctx);
+        assert.isFalse(redirectStub.called);
+        assert.isTrue(normalizeRouteStub.calledOnce);
+        assert.deepEqual(normalizeRouteStub.lastCall.args[0], {
+          changeNum: 1234,
+          basePatchNum: 3,
+          patchNum: 8,
+          view: Gerrit.Nav.View.DIFF,
+          path: 'foo/bar',
+          lineNum: 123,
+          leftSide: true,
+        });
+      });
+
+      test('_handleLegacyLinenum w/ @321', () => {
+        const ctx = {path: '/c/1234/3..8/foo/bar@321'};
+        element._handleLegacyLinenum(ctx);
+        assert.isTrue(redirectStub.calledOnce);
+        assert.isTrue(redirectStub.calledWithExactly(
+            '/c/1234/3..8/foo/bar#321'));
+      });
+
+      test('_handleLegacyLinenum w/ @b123', () => {
+        const ctx = {path: '/c/1234/3..8/foo/bar@b123'};
+        element._handleLegacyLinenum(ctx);
+        assert.isTrue(redirectStub.calledOnce);
+        assert.isTrue(redirectStub.calledWithExactly(
+            '/c/1234/3..8/foo/bar#b123'));
+      });
+
+      suite('_handleChangeRoute', () => {
+        let normalizeRangeStub;
+
+        function makeParams(path, hash) {
+          return {
+            params: [
+              'foo/bar', // 0 Project
+              1234, // 1 Change number
+              null, // 2 Unused
+              null, // 3 Unused
+              4, // 4 Base patch number
+              null, // 5 Unused
+              7, // 6 Patch number
+            ],
+          };
+        }
+
+        setup(() => {
+          normalizeRangeStub = sandbox.stub(element,
+              '_normalizePatchRangeParams');
+          sandbox.stub(element.$.restAPI, 'setInProjectLookup');
+        });
+
+        test('needs redirect', () => {
+          normalizeRangeStub.returns(true);
+          sandbox.stub(element, '_generateUrl').returns('foo');
+          const ctx = makeParams(null, '');
+          element._handleChangeRoute(ctx);
+          assert.isTrue(normalizeRangeStub.called);
+          assert.isFalse(setParamsStub.called);
+          assert.isTrue(redirectStub.calledOnce);
+          assert.isTrue(redirectStub.calledWithExactly('foo'));
+        });
+
+        test('change view', () => {
+          normalizeRangeStub.returns(false);
+          sandbox.stub(element, '_generateUrl').returns('foo');
+          const ctx = makeParams(null, '');
+          assertDataToParams(ctx, '_handleChangeRoute', {
+            view: Gerrit.Nav.View.CHANGE,
+            project: 'foo/bar',
+            changeNum: 1234,
+            basePatchNum: 4,
+            patchNum: 7,
+          });
+          assert.isFalse(redirectStub.called);
+          assert.isTrue(normalizeRangeStub.called);
+        });
+      });
+
+      suite('_handleDiffRoute', () => {
+        let normalizeRangeStub;
+
+        function makeParams(path, hash) {
+          return {
+            params: [
+              'foo/bar', // 0 Project
+              1234, // 1 Change number
+              null, // 2 Unused
+              null, // 3 Unused
+              4, // 4 Base patch number
+              null, // 5 Unused
+              7, // 6 Patch number
+              null, // 7 Unused,
+              path, // 8 Diff path
+            ],
+            hash,
+          };
+        }
+
+        setup(() => {
+          normalizeRangeStub = sandbox.stub(element,
+              '_normalizePatchRangeParams');
+          sandbox.stub(element.$.restAPI, 'setInProjectLookup');
+        });
+
+        test('needs redirect', () => {
+          normalizeRangeStub.returns(true);
+          sandbox.stub(element, '_generateUrl').returns('foo');
+          const ctx = makeParams(null, '');
+          element._handleDiffRoute(ctx);
+          assert.isTrue(normalizeRangeStub.called);
+          assert.isFalse(setParamsStub.called);
+          assert.isTrue(redirectStub.calledOnce);
+          assert.isTrue(redirectStub.calledWithExactly('foo'));
+        });
+
+        test('diff view', () => {
+          normalizeRangeStub.returns(false);
+          sandbox.stub(element, '_generateUrl').returns('foo');
+          const ctx = makeParams('foo/bar/baz', 'b44');
+          assertDataToParams(ctx, '_handleDiffRoute', {
+            view: Gerrit.Nav.View.DIFF,
+            project: 'foo/bar',
+            changeNum: 1234,
+            basePatchNum: 4,
+            patchNum: 7,
+            path: 'foo/bar/baz',
+            leftSide: true,
+            lineNum: 44,
+          });
+          assert.isFalse(redirectStub.called);
+          assert.isTrue(normalizeRangeStub.called);
+        });
+      });
+
+      test('_handleDiffEditRoute', () => {
+        const normalizeRangeSpy =
+            sandbox.spy(element, '_normalizePatchRangeParams');
+        sandbox.stub(element.$.restAPI, 'setInProjectLookup');
+        const ctx = {
+          params: [
+            'foo/bar', // 0 Project
+            1234, // 1 Change number
+            3, // 2 Patch num
+            'foo/bar/baz', // 3 File path
+          ],
+        };
+        const appParams = {
+          project: 'foo/bar',
+          changeNum: 1234,
+          view: Gerrit.Nav.View.EDIT,
+          path: 'foo/bar/baz',
+          patchNum: 3,
+          lineNum: undefined,
+        };
+
+        element._handleDiffEditRoute(ctx);
+        assert.isFalse(redirectStub.called);
+        assert.isTrue(normalizeRangeSpy.calledOnce);
+        assert.deepEqual(normalizeRangeSpy.lastCall.args[0], appParams);
+        assert.isFalse(normalizeRangeSpy.lastCall.returnValue);
+        assert.deepEqual(setParamsStub.lastCall.args[0], appParams);
+      });
+
+      test('_handleDiffEditRoute with lineNum', () => {
+        const normalizeRangeSpy =
+            sandbox.spy(element, '_normalizePatchRangeParams');
+        sandbox.stub(element.$.restAPI, 'setInProjectLookup');
+        const ctx = {
+          params: [
+            'foo/bar', // 0 Project
+            1234, // 1 Change number
+            3, // 2 Patch num
+            'foo/bar/baz', // 3 File path
+          ],
+          hash: 4,
+        };
+        const appParams = {
+          project: 'foo/bar',
+          changeNum: 1234,
+          view: Gerrit.Nav.View.EDIT,
+          path: 'foo/bar/baz',
+          patchNum: 3,
+          lineNum: 4,
+        };
+
+        element._handleDiffEditRoute(ctx);
+        assert.isFalse(redirectStub.called);
+        assert.isTrue(normalizeRangeSpy.calledOnce);
+        assert.deepEqual(normalizeRangeSpy.lastCall.args[0], appParams);
+        assert.isFalse(normalizeRangeSpy.lastCall.returnValue);
+        assert.deepEqual(setParamsStub.lastCall.args[0], appParams);
+      });
+
+      test('_handleChangeEditRoute', () => {
+        const normalizeRangeSpy =
+            sandbox.spy(element, '_normalizePatchRangeParams');
+        sandbox.stub(element.$.restAPI, 'setInProjectLookup');
+        const ctx = {
+          params: [
+            'foo/bar', // 0 Project
+            1234, // 1 Change number
+            null,
+            3, // 3 Patch num
+          ],
+        };
+        const appParams = {
+          project: 'foo/bar',
+          changeNum: 1234,
+          view: Gerrit.Nav.View.CHANGE,
+          patchNum: 3,
+          edit: true,
+        };
+
+        element._handleChangeEditRoute(ctx);
+        assert.isFalse(redirectStub.called);
+        assert.isTrue(normalizeRangeSpy.calledOnce);
+        assert.deepEqual(normalizeRangeSpy.lastCall.args[0], appParams);
+        assert.isFalse(normalizeRangeSpy.lastCall.returnValue);
+        assert.deepEqual(setParamsStub.lastCall.args[0], appParams);
+      });
+    });
+
+    test('_handlePluginScreen', () => {
+      const ctx = {params: ['foo', 'bar']};
+      assertDataToParams(ctx, '_handlePluginScreen', {
+        view: Gerrit.Nav.View.PLUGIN_SCREEN,
+        plugin: 'foo',
+        screen: 'bar',
+      });
+      assert.isFalse(redirectStub.called);
     });
   });
+
+  suite('_parseQueryString', () => {
+    test('empty queries', () => {
+      assert.deepEqual(element._parseQueryString(''), []);
+      assert.deepEqual(element._parseQueryString('?'), []);
+      assert.deepEqual(element._parseQueryString('??'), []);
+      assert.deepEqual(element._parseQueryString('&&&'), []);
+    });
+
+    test('url decoding', () => {
+      assert.deepEqual(element._parseQueryString('+'), [[' ', '']]);
+      assert.deepEqual(element._parseQueryString('???+%3d+'), [[' = ', '']]);
+      assert.deepEqual(
+          element._parseQueryString('%6e%61%6d%65=%76%61%6c%75%65'),
+          [['name', 'value']]);
+    });
+
+    test('multiple parameters', () => {
+      assert.deepEqual(
+          element._parseQueryString('a=b&c=d&e=f'),
+          [['a', 'b'], ['c', 'd'], ['e', 'f']]);
+      assert.deepEqual(
+          element._parseQueryString('&a=b&&&e=f&'),
+          [['a', 'b'], ['e', 'f']]);
+    });
+  });
+});
 </script>
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
index 41caab5..0ed5291 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
@@ -1,6 +1,6 @@
 /**
  * @license
- * Copyright (C) 2016 The Android Open Source Project
+ * 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.
@@ -14,320 +14,332 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-(function() {
-  'use strict';
+import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
 
-  // Possible static search options for auto complete, without negations.
-  const SEARCH_OPERATORS = [
-    'added:',
-    'age:',
-    'age:1week', // Give an example age
-    'assignee:',
-    'author:',
-    'branch:',
-    'bug:',
-    'cc:',
-    'cc:self',
-    'change:',
-    'cherrypickof:',
-    'comment:',
-    'commentby:',
-    'commit:',
-    'committer:',
-    'conflicts:',
-    'deleted:',
-    'delta:',
-    'dir:',
-    'directory:',
-    'ext:',
-    'extension:',
-    'file:',
-    'footer:',
-    'from:',
-    'has:',
-    'has:draft',
-    'has:edit',
-    'has:star',
-    'has:stars',
-    'has:unresolved',
-    'hashtag:',
-    'intopic:',
-    'is:',
-    'is:abandoned',
-    'is:assigned',
-    'is:closed',
-    'is:ignored',
-    'is:merged',
-    'is:open',
-    'is:owner',
-    'is:private',
-    'is:reviewed',
-    'is:reviewer',
-    'is:starred',
-    'is:submittable',
-    'is:watched',
-    'is:wip',
-    'label:',
-    'message:',
-    'onlyexts:',
-    'onlyextensions:',
-    'owner:',
-    'ownerin:',
-    'parentproject:',
-    'project:',
-    'projects:',
-    'query:',
-    'ref:',
-    'reviewedby:',
-    'reviewer:',
-    'reviewer:self',
-    'reviewerin:',
-    'size:',
-    'star:',
-    'status:',
-    'status:abandoned',
-    'status:closed',
-    'status:merged',
-    'status:open',
-    'status:reviewed',
-    'submissionid:',
-    'topic:',
-    'tr:',
-  ];
+import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import '../../../scripts/bundled-polymer.js';
+import '../../shared/gr-autocomplete/gr-autocomplete.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../../styles/shared-styles.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-search-bar_html.js';
 
-  // All of the ops, with corresponding negations.
-  const SEARCH_OPERATORS_WITH_NEGATIONS_SET =
-    new Set(SEARCH_OPERATORS.concat(SEARCH_OPERATORS.map(op => `-${op}`)));
+// Possible static search options for auto complete, without negations.
+const SEARCH_OPERATORS = [
+  'added:',
+  'age:',
+  'age:1week', // Give an example age
+  'assignee:',
+  'author:',
+  'branch:',
+  'bug:',
+  'cc:',
+  'cc:self',
+  'change:',
+  'cherrypickof:',
+  'comment:',
+  'commentby:',
+  'commit:',
+  'committer:',
+  'conflicts:',
+  'deleted:',
+  'delta:',
+  'dir:',
+  'directory:',
+  'ext:',
+  'extension:',
+  'file:',
+  'footer:',
+  'from:',
+  'has:',
+  'has:draft',
+  'has:edit',
+  'has:star',
+  'has:stars',
+  'has:unresolved',
+  'hashtag:',
+  'intopic:',
+  'is:',
+  'is:abandoned',
+  'is:assigned',
+  'is:closed',
+  'is:ignored',
+  'is:merged',
+  'is:open',
+  'is:owner',
+  'is:private',
+  'is:reviewed',
+  'is:reviewer',
+  'is:starred',
+  'is:submittable',
+  'is:watched',
+  'is:wip',
+  'label:',
+  'message:',
+  'onlyexts:',
+  'onlyextensions:',
+  'owner:',
+  'ownerin:',
+  'parentproject:',
+  'project:',
+  'projects:',
+  'query:',
+  'ref:',
+  'reviewedby:',
+  'reviewer:',
+  'reviewer:self',
+  'reviewerin:',
+  'size:',
+  'star:',
+  'status:',
+  'status:abandoned',
+  'status:closed',
+  'status:merged',
+  'status:open',
+  'status:reviewed',
+  'submissionid:',
+  'topic:',
+  'tr:',
+];
 
-  const MAX_AUTOCOMPLETE_RESULTS = 10;
+// All of the ops, with corresponding negations.
+const SEARCH_OPERATORS_WITH_NEGATIONS_SET =
+  new Set(SEARCH_OPERATORS.concat(SEARCH_OPERATORS.map(op => `-${op}`)));
 
-  const TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+\s*/g;
+const MAX_AUTOCOMPLETE_RESULTS = 10;
 
+const TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+\s*/g;
+
+/**
+ * @appliesMixin Gerrit.KeyboardShortcutMixin
+ * @appliesMixin Gerrit.URLEncodingMixin
+ * @extends Polymer.Element
+ */
+class GrSearchBar extends mixinBehaviors( [
+  Gerrit.KeyboardShortcutBehavior,
+  Gerrit.URLEncodingBehavior,
+], GestureEventListeners(
+    LegacyElementMixin(
+        PolymerElement))) {
+  static get template() { return htmlTemplate; }
+
+  static get is() { return 'gr-search-bar'; }
   /**
-   * @appliesMixin Gerrit.KeyboardShortcutMixin
-   * @appliesMixin Gerrit.URLEncodingMixin
-   * @extends Polymer.Element
+   * Fired when a search is committed
+   *
+   * @event handle-search
    */
-  class GrSearchBar extends Polymer.mixinBehaviors( [
-    Gerrit.KeyboardShortcutBehavior,
-    Gerrit.URLEncodingBehavior,
-  ], Polymer.GestureEventListeners(
-      Polymer.LegacyElementMixin(
-          Polymer.Element))) {
-    static get is() { return 'gr-search-bar'; }
-    /**
-     * Fired when a search is committed
-     *
-     * @event handle-search
-     */
 
-    static get properties() {
-      return {
-        value: {
-          type: String,
-          value: '',
-          notify: true,
-          observer: '_valueChanged',
+  static get properties() {
+    return {
+      value: {
+        type: String,
+        value: '',
+        notify: true,
+        observer: '_valueChanged',
+      },
+      keyEventTarget: {
+        type: Object,
+        value() { return document.body; },
+      },
+      query: {
+        type: Function,
+        value() {
+          return this._getSearchSuggestions.bind(this);
         },
-        keyEventTarget: {
-          type: Object,
-          value() { return document.body; },
+      },
+      projectSuggestions: {
+        type: Function,
+        value() {
+          return () => Promise.resolve([]);
         },
-        query: {
-          type: Function,
-          value() {
-            return this._getSearchSuggestions.bind(this);
-          },
+      },
+      groupSuggestions: {
+        type: Function,
+        value() {
+          return () => Promise.resolve([]);
         },
-        projectSuggestions: {
-          type: Function,
-          value() {
-            return () => Promise.resolve([]);
-          },
+      },
+      accountSuggestions: {
+        type: Function,
+        value() {
+          return () => Promise.resolve([]);
         },
-        groupSuggestions: {
-          type: Function,
-          value() {
-            return () => Promise.resolve([]);
-          },
-        },
-        accountSuggestions: {
-          type: Function,
-          value() {
-            return () => Promise.resolve([]);
-          },
-        },
-        _inputVal: String,
-        _threshold: {
-          type: Number,
-          value: 1,
-        },
-      };
-    }
+      },
+      _inputVal: String,
+      _threshold: {
+        type: Number,
+        value: 1,
+      },
+    };
+  }
 
-    attached() {
-      super.attached();
-      this.$.restAPI.getConfig().then(serverConfig => {
-        const mergeability = serverConfig
-         && serverConfig.index
-          && serverConfig.index.mergeabilityComputationBehavior;
-        if (mergeability === 'API_REF_UPDATED_AND_CHANGE_REINDEX'
-        || mergeability === 'REF_UPDATED_AND_CHANGE_REINDEX') {
-          // add 'is:mergeable' to SEARCH_OPERATORS_WITH_NEGATIONS_SET
-          this._addOperator('is:mergeable');
-        }
-      });
-    }
-
-    _addOperator(name, include_neg = true) {
-      SEARCH_OPERATORS_WITH_NEGATIONS_SET.add(name);
-      if (include_neg) {
-        SEARCH_OPERATORS_WITH_NEGATIONS_SET.add(`-${name}`);
+  attached() {
+    super.attached();
+    this.$.restAPI.getConfig().then(serverConfig => {
+      const mergeability = serverConfig
+       && serverConfig.index
+        && serverConfig.index.mergeabilityComputationBehavior;
+      if (mergeability === 'API_REF_UPDATED_AND_CHANGE_REINDEX'
+      || mergeability === 'REF_UPDATED_AND_CHANGE_REINDEX') {
+        // add 'is:mergeable' to SEARCH_OPERATORS_WITH_NEGATIONS_SET
+        this._addOperator('is:mergeable');
       }
-    }
+    });
+  }
 
-    keyboardShortcuts() {
-      return {
-        [this.Shortcut.SEARCH]: '_handleSearch',
-      };
-    }
-
-    _valueChanged(value) {
-      this._inputVal = value;
-    }
-
-    _handleInputCommit(e) {
-      this._preventDefaultAndNavigateToInputVal(e);
-    }
-
-    /**
-     * This function is called in a few different cases:
-     *   - e.target is the search button
-     *   - e.target is the gr-autocomplete widget (#searchInput)
-     *   - e.target is the input element wrapped within #searchInput
-     *
-     * @param {!Event} e
-     */
-    _preventDefaultAndNavigateToInputVal(e) {
-      e.preventDefault();
-      const target = Polymer.dom(e).rootTarget;
-      // If the target is the #searchInput or has a sub-input component, that
-      // is what holds the focus as opposed to the target from the DOM event.
-      if (target.$.input) {
-        target.$.input.blur();
-      } else {
-        target.blur();
-      }
-      const trimmedInput = this._inputVal && this._inputVal.trim();
-      if (trimmedInput) {
-        const predefinedOpOnlyQuery = [...SEARCH_OPERATORS_WITH_NEGATIONS_SET]
-            .some(op => op.endsWith(':') && op === trimmedInput);
-        if (predefinedOpOnlyQuery) {
-          return;
-        }
-        this.dispatchEvent(new CustomEvent('handle-search', {
-          detail: {inputVal: this._inputVal},
-        }));
-      }
-    }
-
-    /**
-     * Determine what array of possible suggestions should be provided
-     *     to _getSearchSuggestions.
-     *
-     * @param {string} input - The full search term, in lowercase.
-     * @return {!Promise} This returns a promise that resolves to an array of
-     *     suggestion objects.
-     */
-    _fetchSuggestions(input) {
-      // Split the input on colon to get a two part predicate/expression.
-      const splitInput = input.split(':');
-      const predicate = splitInput[0];
-      const expression = splitInput[1] || '';
-      // Switch on the predicate to determine what to autocomplete.
-      switch (predicate) {
-        case 'ownerin':
-        case 'reviewerin':
-          // Fetch groups.
-          return this.groupSuggestions(predicate, expression);
-
-        case 'parentproject':
-        case 'project':
-          // Fetch projects.
-          return this.projectSuggestions(predicate, expression);
-
-        case 'author':
-        case 'cc':
-        case 'commentby':
-        case 'committer':
-        case 'from':
-        case 'owner':
-        case 'reviewedby':
-        case 'reviewer':
-          // Fetch accounts.
-          return this.accountSuggestions(predicate, expression);
-
-        default:
-          return Promise.resolve([...SEARCH_OPERATORS_WITH_NEGATIONS_SET]
-              .filter(operator => operator.includes(input))
-              .map(operator => { return {text: operator}; }));
-      }
-    }
-
-    /**
-     * Get the sorted, pruned list of suggestions for the current search query.
-     *
-     * @param {string} input - The complete search query.
-     * @return {!Promise} This returns a promise that resolves to an array of
-     *     suggestions.
-     */
-    _getSearchSuggestions(input) {
-      // Allow spaces within quoted terms.
-      const tokens = input.match(TOKENIZE_REGEX);
-      const trimmedInput = tokens[tokens.length - 1].toLowerCase();
-
-      return this._fetchSuggestions(trimmedInput)
-          .then(suggestions => {
-            if (!suggestions || !suggestions.length) { return []; }
-            return suggestions
-                // Prioritize results that start with the input.
-                .sort((a, b) => {
-                  const aContains = a.text.toLowerCase().indexOf(trimmedInput);
-                  const bContains = b.text.toLowerCase().indexOf(trimmedInput);
-                  if (aContains === bContains) {
-                    return a.text.localeCompare(b.text);
-                  }
-                  if (aContains === -1) {
-                    return 1;
-                  }
-                  if (bContains === -1) {
-                    return -1;
-                  }
-                  return aContains - bContains;
-                })
-                // Return only the first {MAX_AUTOCOMPLETE_RESULTS} results.
-                .slice(0, MAX_AUTOCOMPLETE_RESULTS - 1)
-                // Map to an object to play nice with gr-autocomplete.
-                .map(({text, label}) => {
-                  return {
-                    name: text,
-                    value: text,
-                    label,
-                  };
-                });
-          });
-    }
-
-    _handleSearch(e) {
-      const keyboardEvent = this.getKeyboardEvent(e);
-      if (this.shouldSuppressKeyboardShortcut(e) ||
-          (this.modifierPressed(e) && !keyboardEvent.shiftKey)) { return; }
-
-      e.preventDefault();
-      this.$.searchInput.focus();
-      this.$.searchInput.selectAll();
+  _addOperator(name, include_neg = true) {
+    SEARCH_OPERATORS_WITH_NEGATIONS_SET.add(name);
+    if (include_neg) {
+      SEARCH_OPERATORS_WITH_NEGATIONS_SET.add(`-${name}`);
     }
   }
 
-  customElements.define(GrSearchBar.is, GrSearchBar);
-})();
+  keyboardShortcuts() {
+    return {
+      [this.Shortcut.SEARCH]: '_handleSearch',
+    };
+  }
+
+  _valueChanged(value) {
+    this._inputVal = value;
+  }
+
+  _handleInputCommit(e) {
+    this._preventDefaultAndNavigateToInputVal(e);
+  }
+
+  /**
+   * This function is called in a few different cases:
+   *   - e.target is the search button
+   *   - e.target is the gr-autocomplete widget (#searchInput)
+   *   - e.target is the input element wrapped within #searchInput
+   *
+   * @param {!Event} e
+   */
+  _preventDefaultAndNavigateToInputVal(e) {
+    e.preventDefault();
+    const target = dom(e).rootTarget;
+    // If the target is the #searchInput or has a sub-input component, that
+    // is what holds the focus as opposed to the target from the DOM event.
+    if (target.$.input) {
+      target.$.input.blur();
+    } else {
+      target.blur();
+    }
+    const trimmedInput = this._inputVal && this._inputVal.trim();
+    if (trimmedInput) {
+      const predefinedOpOnlyQuery = [...SEARCH_OPERATORS_WITH_NEGATIONS_SET]
+          .some(op => op.endsWith(':') && op === trimmedInput);
+      if (predefinedOpOnlyQuery) {
+        return;
+      }
+      this.dispatchEvent(new CustomEvent('handle-search', {
+        detail: {inputVal: this._inputVal},
+      }));
+    }
+  }
+
+  /**
+   * Determine what array of possible suggestions should be provided
+   *     to _getSearchSuggestions.
+   *
+   * @param {string} input - The full search term, in lowercase.
+   * @return {!Promise} This returns a promise that resolves to an array of
+   *     suggestion objects.
+   */
+  _fetchSuggestions(input) {
+    // Split the input on colon to get a two part predicate/expression.
+    const splitInput = input.split(':');
+    const predicate = splitInput[0];
+    const expression = splitInput[1] || '';
+    // Switch on the predicate to determine what to autocomplete.
+    switch (predicate) {
+      case 'ownerin':
+      case 'reviewerin':
+        // Fetch groups.
+        return this.groupSuggestions(predicate, expression);
+
+      case 'parentproject':
+      case 'project':
+        // Fetch projects.
+        return this.projectSuggestions(predicate, expression);
+
+      case 'author':
+      case 'cc':
+      case 'commentby':
+      case 'committer':
+      case 'from':
+      case 'owner':
+      case 'reviewedby':
+      case 'reviewer':
+        // Fetch accounts.
+        return this.accountSuggestions(predicate, expression);
+
+      default:
+        return Promise.resolve([...SEARCH_OPERATORS_WITH_NEGATIONS_SET]
+            .filter(operator => operator.includes(input))
+            .map(operator => { return {text: operator}; }));
+    }
+  }
+
+  /**
+   * Get the sorted, pruned list of suggestions for the current search query.
+   *
+   * @param {string} input - The complete search query.
+   * @return {!Promise} This returns a promise that resolves to an array of
+   *     suggestions.
+   */
+  _getSearchSuggestions(input) {
+    // Allow spaces within quoted terms.
+    const tokens = input.match(TOKENIZE_REGEX);
+    const trimmedInput = tokens[tokens.length - 1].toLowerCase();
+
+    return this._fetchSuggestions(trimmedInput)
+        .then(suggestions => {
+          if (!suggestions || !suggestions.length) { return []; }
+          return suggestions
+              // Prioritize results that start with the input.
+              .sort((a, b) => {
+                const aContains = a.text.toLowerCase().indexOf(trimmedInput);
+                const bContains = b.text.toLowerCase().indexOf(trimmedInput);
+                if (aContains === bContains) {
+                  return a.text.localeCompare(b.text);
+                }
+                if (aContains === -1) {
+                  return 1;
+                }
+                if (bContains === -1) {
+                  return -1;
+                }
+                return aContains - bContains;
+              })
+              // Return only the first {MAX_AUTOCOMPLETE_RESULTS} results.
+              .slice(0, MAX_AUTOCOMPLETE_RESULTS - 1)
+              // Map to an object to play nice with gr-autocomplete.
+              .map(({text, label}) => {
+                return {
+                  name: text,
+                  value: text,
+                  label,
+                };
+              });
+        });
+  }
+
+  _handleSearch(e) {
+    const keyboardEvent = this.getKeyboardEvent(e);
+    if (this.shouldSuppressKeyboardShortcut(e) ||
+        (this.modifierPressed(e) && !keyboardEvent.shiftKey)) { return; }
+
+    e.preventDefault();
+    this.$.searchInput.focus();
+    this.$.searchInput.selectAll();
+  }
+}
+
+customElements.define(GrSearchBar.is, GrSearchBar);
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_html.js b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_html.js
index cb8e142..831b080 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_html.js
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_html.js
@@ -1,29 +1,22 @@
-<!--
-@license
-Copyright (C) 2015 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.
+ */
+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.
--->
-
-<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
-<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-search-bar">
-  <template>
+export const htmlTemplate = html`
     <style include="shared-styles">
       form {
         display: flex;
@@ -36,19 +29,7 @@
       }
     </style>
     <form>
-      <gr-autocomplete
-          show-search-icon
-          id="searchInput"
-          text="{{_inputVal}}"
-          query="[[query]]"
-          on-commit="_handleInputCommit"
-          allow-non-suggested-values
-          multi
-          threshold="[[_threshold]]"
-          tab-complete
-          vertical-offset="30"></gr-autocomplete>
+      <gr-autocomplete show-search-icon="" id="searchInput" text="{{_inputVal}}" query="[[query]]" on-commit="_handleInputCommit" allow-non-suggested-values="" multi="" threshold="[[_threshold]]" tab-complete="" vertical-offset="30"></gr-autocomplete>
     </form>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
-  </template>
-  <script src="gr-search-bar.js"></script>
-</dom-module>
+`;
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.html
index 5b5dc02..1bd0fca 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.html
@@ -19,18 +19,24 @@
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-search-bar</title>
 
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="/bower_components/page/page.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
+<script type="module" src="../../../test/test-pre-setup.js"></script>
+<script type="module" src="../../../test/common-test-setup.js"></script>
+<script src="/node_modules/page/page.js"></script>
 
-<link rel="import" href="gr-search-bar.html">
-<script src="../../../scripts/util.js"></script>
+<script type="module" src="./gr-search-bar.js"></script>
+<script type="module" src="../../../scripts/util.js"></script>
 
-<script>void (0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-search-bar.js';
+import '../../../scripts/util.js';
+void (0);
+</script>
 
 <test-fixture id="basic">
   <template>
@@ -38,199 +44,202 @@
   </template>
 </test-fixture>
 
-<script>
-  suite('gr-search-bar tests', async () => {
-    await readyToTest();
-    const kb = window.Gerrit.KeyboardShortcutBinder;
-    kb.bindShortcut(kb.Shortcut.SEARCH, '/');
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-search-bar.js';
+import '../../../scripts/util.js';
+suite('gr-search-bar tests', () => {
+  const kb = window.Gerrit.KeyboardShortcutBinder;
+  kb.bindShortcut(kb.Shortcut.SEARCH, '/');
 
-    let element;
-    let sandbox;
+  let element;
+  let sandbox;
 
-    setup(done => {
-      sandbox = sinon.sandbox.create();
-      element = fixture('basic');
-      flush(done);
+  setup(done => {
+    sandbox = sinon.sandbox.create();
+    element = fixture('basic');
+    flush(done);
+  });
+
+  teardown(() => {
+    sandbox.restore();
+  });
+
+  test('value is propagated to _inputVal', () => {
+    element.value = 'foo';
+    assert.equal(element._inputVal, 'foo');
+  });
+
+  const getActiveElement = () => (document.activeElement.shadowRoot ?
+    document.activeElement.shadowRoot.activeElement :
+    document.activeElement);
+
+  test('enter in search input fires event', done => {
+    element.addEventListener('handle-search', () => {
+      assert.notEqual(getActiveElement(), element.$.searchInput);
+      assert.notEqual(getActiveElement(), element.$.searchButton);
+      done();
+    });
+    element.value = 'test';
+    MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
+        null, 'enter');
+  });
+
+  test('input blurred after commit', () => {
+    const blurSpy = sandbox.spy(element.$.searchInput.$.input, 'blur');
+    element.$.searchInput.text = 'fate/stay';
+    MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
+        null, 'enter');
+    assert.isTrue(blurSpy.called);
+  });
+
+  test('empty search query does not trigger nav', () => {
+    const searchSpy = sandbox.spy();
+    element.addEventListener('handle-search', searchSpy);
+    element.value = '';
+    MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
+        null, 'enter');
+    assert.isFalse(searchSpy.called);
+  });
+
+  test('Predefined query op with no predication doesnt trigger nav', () => {
+    const searchSpy = sandbox.spy();
+    element.addEventListener('handle-search', searchSpy);
+    element.value = 'added:';
+    MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
+        null, 'enter');
+    assert.isFalse(searchSpy.called);
+  });
+
+  test('predefined predicate query triggers nav', () => {
+    const searchSpy = sandbox.spy();
+    element.addEventListener('handle-search', searchSpy);
+    element.value = 'age:1week';
+    MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
+        null, 'enter');
+    assert.isTrue(searchSpy.called);
+  });
+
+  test('undefined predicate query triggers nav', () => {
+    const searchSpy = sandbox.spy();
+    element.addEventListener('handle-search', searchSpy);
+    element.value = 'random:1week';
+    MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
+        null, 'enter');
+    assert.isTrue(searchSpy.called);
+  });
+
+  test('empty undefined predicate query triggers nav', () => {
+    const searchSpy = sandbox.spy();
+    element.addEventListener('handle-search', searchSpy);
+    element.value = 'random:';
+    MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
+        null, 'enter');
+    assert.isTrue(searchSpy.called);
+  });
+
+  test('keyboard shortcuts', () => {
+    const focusSpy = sandbox.spy(element.$.searchInput, 'focus');
+    const selectAllSpy = sandbox.spy(element.$.searchInput, 'selectAll');
+    MockInteractions.pressAndReleaseKeyOn(document.body, 191, null, '/');
+    assert.isTrue(focusSpy.called);
+    assert.isTrue(selectAllSpy.called);
+  });
+
+  suite('_getSearchSuggestions', () => {
+    test('Autocompletes accounts', () => {
+      sandbox.stub(element, 'accountSuggestions', () =>
+        Promise.resolve([{text: 'owner:fred@goog.co'}])
+      );
+      return element._getSearchSuggestions('owner:fr').then(s => {
+        assert.equal(s[0].value, 'owner:fred@goog.co');
+      });
     });
 
-    teardown(() => {
-      sandbox.restore();
-    });
-
-    test('value is propagated to _inputVal', () => {
-      element.value = 'foo';
-      assert.equal(element._inputVal, 'foo');
-    });
-
-    const getActiveElement = () => (document.activeElement.shadowRoot ?
-      document.activeElement.shadowRoot.activeElement :
-      document.activeElement);
-
-    test('enter in search input fires event', done => {
-      element.addEventListener('handle-search', () => {
-        assert.notEqual(getActiveElement(), element.$.searchInput);
-        assert.notEqual(getActiveElement(), element.$.searchButton);
+    test('Autocompletes groups', done => {
+      sandbox.stub(element, 'groupSuggestions', () =>
+        Promise.resolve([
+          {text: 'ownerin:Polygerrit'},
+          {text: 'ownerin:gerrit'},
+        ])
+      );
+      element._getSearchSuggestions('ownerin:pol').then(s => {
+        assert.equal(s[0].value, 'ownerin:Polygerrit');
         done();
       });
-      element.value = 'test';
-      MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
-          null, 'enter');
     });
 
-    test('input blurred after commit', () => {
-      const blurSpy = sandbox.spy(element.$.searchInput.$.input, 'blur');
-      element.$.searchInput.text = 'fate/stay';
-      MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
-          null, 'enter');
-      assert.isTrue(blurSpy.called);
+    test('Autocompletes projects', done => {
+      sandbox.stub(element, 'projectSuggestions', () =>
+        Promise.resolve([
+          {text: 'project:Polygerrit'},
+          {text: 'project:gerrit'},
+          {text: 'project:gerrittest'},
+        ])
+      );
+      element._getSearchSuggestions('project:pol').then(s => {
+        assert.equal(s[0].value, 'project:Polygerrit');
+        done();
+      });
     });
 
-    test('empty search query does not trigger nav', () => {
-      const searchSpy = sandbox.spy();
-      element.addEventListener('handle-search', searchSpy);
-      element.value = '';
-      MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
-          null, 'enter');
-      assert.isFalse(searchSpy.called);
+    test('Autocompletes simple searches', done => {
+      element._getSearchSuggestions('is:o').then(s => {
+        assert.equal(s[0].name, 'is:open');
+        assert.equal(s[0].value, 'is:open');
+        assert.equal(s[1].name, 'is:owner');
+        assert.equal(s[1].value, 'is:owner');
+        done();
+      });
     });
 
-    test('Predefined query op with no predication doesnt trigger nav', () => {
-      const searchSpy = sandbox.spy();
-      element.addEventListener('handle-search', searchSpy);
-      element.value = 'added:';
-      MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
-          null, 'enter');
-      assert.isFalse(searchSpy.called);
+    test('Does not autocomplete with no match', done => {
+      element._getSearchSuggestions('asdasdasdasd').then(s => {
+        assert.equal(s.length, 0);
+        done();
+      });
     });
 
-    test('predefined predicate query triggers nav', () => {
-      const searchSpy = sandbox.spy();
-      element.addEventListener('handle-search', searchSpy);
-      element.value = 'age:1week';
-      MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
-          null, 'enter');
-      assert.isTrue(searchSpy.called);
+    test('Autocompltes without is:mergable when disabled', done => {
+      element._getSearchSuggestions('is:mergeab').then(s => {
+        assert.equal(s.length, 0);
+        done();
+      });
     });
+  });
 
-    test('undefined predicate query triggers nav', () => {
-      const searchSpy = sandbox.spy();
-      element.addEventListener('handle-search', searchSpy);
-      element.value = 'random:1week';
-      MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
-          null, 'enter');
-      assert.isTrue(searchSpy.called);
-    });
-
-    test('empty undefined predicate query triggers nav', () => {
-      const searchSpy = sandbox.spy();
-      element.addEventListener('handle-search', searchSpy);
-      element.value = 'random:';
-      MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
-          null, 'enter');
-      assert.isTrue(searchSpy.called);
-    });
-
-    test('keyboard shortcuts', () => {
-      const focusSpy = sandbox.spy(element.$.searchInput, 'focus');
-      const selectAllSpy = sandbox.spy(element.$.searchInput, 'selectAll');
-      MockInteractions.pressAndReleaseKeyOn(document.body, 191, null, '/');
-      assert.isTrue(focusSpy.called);
-      assert.isTrue(selectAllSpy.called);
-    });
-
-    suite('_getSearchSuggestions', () => {
-      test('Autocompletes accounts', () => {
-        sandbox.stub(element, 'accountSuggestions', () =>
-          Promise.resolve([{text: 'owner:fred@goog.co'}])
-        );
-        return element._getSearchSuggestions('owner:fr').then(s => {
-          assert.equal(s[0].value, 'owner:fred@goog.co');
+  [
+    'API_REF_UPDATED_AND_CHANGE_REINDEX',
+    'REF_UPDATED_AND_CHANGE_REINDEX',
+  ].forEach(mergeability => {
+    suite(`mergeability as ${mergeability}`, () => {
+      setup(done => {
+        stub('gr-rest-api-interface', {
+          getConfig() {
+            return Promise.resolve({
+              index: {
+                mergeabilityComputationBehavior: mergeability,
+              },
+            });
+          },
         });
+
+        element = fixture('basic');
+        flush(done);
       });
 
-      test('Autocompletes groups', done => {
-        sandbox.stub(element, 'groupSuggestions', () =>
-          Promise.resolve([
-            {text: 'ownerin:Polygerrit'},
-            {text: 'ownerin:gerrit'},
-          ])
-        );
-        element._getSearchSuggestions('ownerin:pol').then(s => {
-          assert.equal(s[0].value, 'ownerin:Polygerrit');
-          done();
-        });
-      });
-
-      test('Autocompletes projects', done => {
-        sandbox.stub(element, 'projectSuggestions', () =>
-          Promise.resolve([
-            {text: 'project:Polygerrit'},
-            {text: 'project:gerrit'},
-            {text: 'project:gerrittest'},
-          ])
-        );
-        element._getSearchSuggestions('project:pol').then(s => {
-          assert.equal(s[0].value, 'project:Polygerrit');
-          done();
-        });
-      });
-
-      test('Autocompletes simple searches', done => {
-        element._getSearchSuggestions('is:o').then(s => {
-          assert.equal(s[0].name, 'is:open');
-          assert.equal(s[0].value, 'is:open');
-          assert.equal(s[1].name, 'is:owner');
-          assert.equal(s[1].value, 'is:owner');
-          done();
-        });
-      });
-
-      test('Does not autocomplete with no match', done => {
-        element._getSearchSuggestions('asdasdasdasd').then(s => {
-          assert.equal(s.length, 0);
-          done();
-        });
-      });
-
-      test('Autocompltes without is:mergable when disabled', done => {
+      test('Autocompltes with is:mergable when enabled', done => {
         element._getSearchSuggestions('is:mergeab').then(s => {
-          assert.equal(s.length, 0);
+          assert.equal(s.length, 2);
+          assert.equal(s[0].name, 'is:mergeable');
+          assert.equal(s[0].value, 'is:mergeable');
+          assert.equal(s[1].name, '-is:mergeable');
+          assert.equal(s[1].value, '-is:mergeable');
           done();
         });
       });
     });
-
-    [
-      'API_REF_UPDATED_AND_CHANGE_REINDEX',
-      'REF_UPDATED_AND_CHANGE_REINDEX',
-    ].forEach(mergeability => {
-      suite(`mergeability as ${mergeability}`, () => {
-        setup(done => {
-          stub('gr-rest-api-interface', {
-            getConfig() {
-              return Promise.resolve({
-                index: {
-                  mergeabilityComputationBehavior: mergeability,
-                },
-              });
-            },
-          });
-
-          element = fixture('basic');
-          flush(done);
-        });
-
-        test('Autocompltes with is:mergable when enabled', done => {
-          element._getSearchSuggestions('is:mergeab').then(s => {
-            assert.equal(s.length, 2);
-            assert.equal(s[0].name, 'is:mergeable');
-            assert.equal(s[0].value, 'is:mergeable');
-            assert.equal(s[1].name, '-is:mergeable');
-            assert.equal(s[1].value, '-is:mergeable');
-            done();
-          });
-        });
-      });
-    });
   });
+});
 </script>
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js
index cfdd524..a93c139 100644
--- a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js
+++ b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js
@@ -14,152 +14,162 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-(function() {
-  'use strict';
+import '../../../scripts/bundled-polymer.js';
 
-  const MAX_AUTOCOMPLETE_RESULTS = 10;
-  const SELF_EXPRESSION = 'self';
-  const ME_EXPRESSION = 'me';
+import '../../../behaviors/gr-display-name-behavior/gr-display-name-behavior.js';
+import '../gr-navigation/gr-navigation.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-search-bar/gr-search-bar.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-smart-search_html.js';
 
-  /**
-   * @appliesMixin Gerrit.DisplayNameMixin
-   * @extends Polymer.Element
-   */
-  class GrSmartSearch extends Polymer.mixinBehaviors( [
-    Gerrit.DisplayNameBehavior,
-  ], Polymer.GestureEventListeners(
-      Polymer.LegacyElementMixin(
-          Polymer.Element))) {
-    static get is() { return 'gr-smart-search'; }
+const MAX_AUTOCOMPLETE_RESULTS = 10;
+const SELF_EXPRESSION = 'self';
+const ME_EXPRESSION = 'me';
 
-    static get properties() {
-      return {
-        searchQuery: String,
-        _config: Object,
-        _projectSuggestions: {
-          type: Function,
-          value() {
-            return this._fetchProjects.bind(this);
-          },
+/**
+ * @appliesMixin Gerrit.DisplayNameMixin
+ * @extends Polymer.Element
+ */
+class GrSmartSearch extends mixinBehaviors( [
+  Gerrit.DisplayNameBehavior,
+], GestureEventListeners(
+    LegacyElementMixin(
+        PolymerElement))) {
+  static get template() { return htmlTemplate; }
+
+  static get is() { return 'gr-smart-search'; }
+
+  static get properties() {
+    return {
+      searchQuery: String,
+      _config: Object,
+      _projectSuggestions: {
+        type: Function,
+        value() {
+          return this._fetchProjects.bind(this);
         },
-        _groupSuggestions: {
-          type: Function,
-          value() {
-            return this._fetchGroups.bind(this);
-          },
+      },
+      _groupSuggestions: {
+        type: Function,
+        value() {
+          return this._fetchGroups.bind(this);
         },
-        _accountSuggestions: {
-          type: Function,
-          value() {
-            return this._fetchAccounts.bind(this);
-          },
+      },
+      _accountSuggestions: {
+        type: Function,
+        value() {
+          return this._fetchAccounts.bind(this);
         },
-      };
-    }
+      },
+    };
+  }
 
-    /** @override */
-    attached() {
-      super.attached();
-      this.$.restAPI.getConfig().then(cfg => {
-        this._config = cfg;
-      });
-    }
+  /** @override */
+  attached() {
+    super.attached();
+    this.$.restAPI.getConfig().then(cfg => {
+      this._config = cfg;
+    });
+  }
 
-    _handleSearch(e) {
-      const input = e.detail.inputVal;
-      if (input) {
-        Gerrit.Nav.navigateToSearchQuery(input);
-      }
-    }
-
-    /**
-     * Fetch from the API the predicted projects.
-     *
-     * @param {string} predicate - The first part of the search term, e.g.
-     *     'project'
-     * @param {string} expression - The second part of the search term, e.g.
-     *     'gerr'
-     * @return {!Promise} This returns a promise that resolves to an array of
-     *     strings.
-     */
-    _fetchProjects(predicate, expression) {
-      return this.$.restAPI.getSuggestedProjects(
-          expression,
-          MAX_AUTOCOMPLETE_RESULTS)
-          .then(projects => {
-            if (!projects) { return []; }
-            const keys = Object.keys(projects);
-            return keys.map(key => { return {text: predicate + ':' + key}; });
-          });
-    }
-
-    /**
-     * Fetch from the API the predicted groups.
-     *
-     * @param {string} predicate - The first part of the search term, e.g.
-     *     'ownerin'
-     * @param {string} expression - The second part of the search term, e.g.
-     *     'polyger'
-     * @return {!Promise} This returns a promise that resolves to an array of
-     *     strings.
-     */
-    _fetchGroups(predicate, expression) {
-      if (expression.length === 0) { return Promise.resolve([]); }
-      return this.$.restAPI.getSuggestedGroups(
-          expression,
-          MAX_AUTOCOMPLETE_RESULTS)
-          .then(groups => {
-            if (!groups) { return []; }
-            const keys = Object.keys(groups);
-            return keys.map(key => { return {text: predicate + ':' + key}; });
-          });
-    }
-
-    /**
-     * Fetch from the API the predicted accounts.
-     *
-     * @param {string} predicate - The first part of the search term, e.g.
-     *     'owner'
-     * @param {string} expression - The second part of the search term, e.g.
-     *     'kasp'
-     * @return {!Promise} This returns a promise that resolves to an array of
-     *     strings.
-     */
-    _fetchAccounts(predicate, expression) {
-      if (expression.length === 0) { return Promise.resolve([]); }
-      return this.$.restAPI.getSuggestedAccounts(
-          expression,
-          MAX_AUTOCOMPLETE_RESULTS)
-          .then(accounts => {
-            if (!accounts) { return []; }
-            return this._mapAccountsHelper(accounts, predicate);
-          })
-          .then(accounts => {
-            // When the expression supplied is a beginning substring of 'self',
-            // add it as an autocomplete option.
-            if (SELF_EXPRESSION.startsWith(expression)) {
-              return accounts.concat(
-                  [{text: predicate + ':' + SELF_EXPRESSION}]);
-            } else if (ME_EXPRESSION.startsWith(expression)) {
-              return accounts.concat([{text: predicate + ':' + ME_EXPRESSION}]);
-            } else {
-              return accounts;
-            }
-          });
-    }
-
-    _mapAccountsHelper(accounts, predicate) {
-      return accounts.map(account => {
-        const userName = this.getUserName(this._serverConfig, account, false);
-        return {
-          label: account.name || '',
-          text: account.email ?
-            `${predicate}:${account.email}` :
-            `${predicate}:"${userName}"`,
-        };
-      });
+  _handleSearch(e) {
+    const input = e.detail.inputVal;
+    if (input) {
+      Gerrit.Nav.navigateToSearchQuery(input);
     }
   }
 
-  customElements.define(GrSmartSearch.is, GrSmartSearch);
-})();
+  /**
+   * Fetch from the API the predicted projects.
+   *
+   * @param {string} predicate - The first part of the search term, e.g.
+   *     'project'
+   * @param {string} expression - The second part of the search term, e.g.
+   *     'gerr'
+   * @return {!Promise} This returns a promise that resolves to an array of
+   *     strings.
+   */
+  _fetchProjects(predicate, expression) {
+    return this.$.restAPI.getSuggestedProjects(
+        expression,
+        MAX_AUTOCOMPLETE_RESULTS)
+        .then(projects => {
+          if (!projects) { return []; }
+          const keys = Object.keys(projects);
+          return keys.map(key => { return {text: predicate + ':' + key}; });
+        });
+  }
+
+  /**
+   * Fetch from the API the predicted groups.
+   *
+   * @param {string} predicate - The first part of the search term, e.g.
+   *     'ownerin'
+   * @param {string} expression - The second part of the search term, e.g.
+   *     'polyger'
+   * @return {!Promise} This returns a promise that resolves to an array of
+   *     strings.
+   */
+  _fetchGroups(predicate, expression) {
+    if (expression.length === 0) { return Promise.resolve([]); }
+    return this.$.restAPI.getSuggestedGroups(
+        expression,
+        MAX_AUTOCOMPLETE_RESULTS)
+        .then(groups => {
+          if (!groups) { return []; }
+          const keys = Object.keys(groups);
+          return keys.map(key => { return {text: predicate + ':' + key}; });
+        });
+  }
+
+  /**
+   * Fetch from the API the predicted accounts.
+   *
+   * @param {string} predicate - The first part of the search term, e.g.
+   *     'owner'
+   * @param {string} expression - The second part of the search term, e.g.
+   *     'kasp'
+   * @return {!Promise} This returns a promise that resolves to an array of
+   *     strings.
+   */
+  _fetchAccounts(predicate, expression) {
+    if (expression.length === 0) { return Promise.resolve([]); }
+    return this.$.restAPI.getSuggestedAccounts(
+        expression,
+        MAX_AUTOCOMPLETE_RESULTS)
+        .then(accounts => {
+          if (!accounts) { return []; }
+          return this._mapAccountsHelper(accounts, predicate);
+        })
+        .then(accounts => {
+          // When the expression supplied is a beginning substring of 'self',
+          // add it as an autocomplete option.
+          if (SELF_EXPRESSION.startsWith(expression)) {
+            return accounts.concat(
+                [{text: predicate + ':' + SELF_EXPRESSION}]);
+          } else if (ME_EXPRESSION.startsWith(expression)) {
+            return accounts.concat([{text: predicate + ':' + ME_EXPRESSION}]);
+          } else {
+            return accounts;
+          }
+        });
+  }
+
+  _mapAccountsHelper(accounts, predicate) {
+    return accounts.map(account => {
+      const userName = this.getUserName(this._serverConfig, account, false);
+      return {
+        label: account.name || '',
+        text: account.email ?
+          `${predicate}:${account.email}` :
+          `${predicate}:"${userName}"`,
+      };
+    });
+  }
+}
+
+customElements.define(GrSmartSearch.is, GrSmartSearch);
diff --git a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_html.js b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_html.js
index c4ae41b..78906a8 100644
--- a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_html.js
+++ b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_html.js
@@ -1,38 +1,25 @@
-<!--
-@license
-Copyright (C) 2016 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.
+ */
+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.
--->
-<link rel="import" href="/bower_components/polymer/polymer.html">
-
-<link rel="import" href="../../../behaviors/gr-display-name-behavior/gr-display-name-behavior.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../gr-search-bar/gr-search-bar.html">
-
-<dom-module id="gr-smart-search">
-  <template>
+export const htmlTemplate = html`
     <style include="shared-styles">
 
     </style>
-    <gr-search-bar id="search"
-        value="{{searchQuery}}"
-        on-handle-search="_handleSearch"
-        project-suggestions="[[_projectSuggestions]]"
-        group-suggestions="[[_groupSuggestions]]"
-        account-suggestions="[[_accountSuggestions]]"></gr-search-bar>
+    <gr-search-bar id="search" value="{{searchQuery}}" on-handle-search="_handleSearch" project-suggestions="[[_projectSuggestions]]" group-suggestions="[[_groupSuggestions]]" account-suggestions="[[_accountSuggestions]]"></gr-search-bar>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
-  </template>
-  <script src="gr-smart-search.js"></script>
-</dom-module>
+`;
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.html
index 6fd00c7..a0ba203 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.html
@@ -19,15 +19,20 @@
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-smart-search</title>
 
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-smart-search.html">
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
+<script type="module" src="../../../test/test-pre-setup.js"></script>
+<script type="module" src="../../../test/common-test-setup.js"></script>
+<script type="module" src="./gr-smart-search.js"></script>
 
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-smart-search.js';
+void(0);
+</script>
 
 <test-fixture id="basic">
   <template>
@@ -35,125 +40,127 @@
   </template>
 </test-fixture>
 
-<script>
-  suite('gr-smart-search tests', async () => {
-    await readyToTest();
-    let element;
-    let sandbox;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-smart-search.js';
+suite('gr-smart-search tests', () => {
+  let element;
+  let sandbox;
 
-    setup(() => {
-      sandbox = sinon.sandbox.create();
-      element = fixture('basic');
-    });
+  setup(() => {
+    sandbox = sinon.sandbox.create();
+    element = fixture('basic');
+  });
 
-    teardown(() => {
-      sandbox.restore();
-    });
+  teardown(() => {
+    sandbox.restore();
+  });
 
-    test('Autocompletes accounts', () => {
-      sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () =>
-        Promise.resolve([
-          {
-            name: 'fred',
-            email: 'fred@goog.co',
-          },
-        ])
-      );
-      return element._fetchAccounts('owner', 'fr').then(s => {
-        assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: 'fred'});
-      });
-    });
-
-    test('Inserts self as option when valid', () => {
-      sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () =>
-        Promise.resolve([
-          {
-            name: 'fred',
-            email: 'fred@goog.co',
-          },
-        ])
-      );
-      element._fetchAccounts('owner', 's')
-          .then(s => {
-            assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: 'fred'});
-            assert.deepEqual(s[1], {text: 'owner:self'});
-          })
-          .then(() => element._fetchAccounts('owner', 'selfs'))
-          .then(s => {
-            assert.notEqual(s[0], {text: 'owner:self'});
-          });
-    });
-
-    test('Inserts me as option when valid', () => {
-      sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () =>
-        Promise.resolve([
-          {
-            name: 'fred',
-            email: 'fred@goog.co',
-          },
-        ])
-      );
-      return element._fetchAccounts('owner', 'm')
-          .then(s => {
-            assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: 'fred'});
-            assert.deepEqual(s[1], {text: 'owner:me'});
-          })
-          .then(() => element._fetchAccounts('owner', 'meme'))
-          .then(s => {
-            assert.notEqual(s[0], {text: 'owner:me'});
-          });
-    });
-
-    test('Autocompletes groups', () => {
-      sandbox.stub(element.$.restAPI, 'getSuggestedGroups', () =>
-        Promise.resolve({
-          Polygerrit: 0,
-          gerrit: 0,
-          gerrittest: 0,
-        })
-      );
-      return element._fetchGroups('ownerin', 'pol').then(s => {
-        assert.deepEqual(s[0], {text: 'ownerin:Polygerrit'});
-      });
-    });
-
-    test('Autocompletes projects', () => {
-      sandbox.stub(element.$.restAPI, 'getSuggestedProjects', () =>
-        Promise.resolve({Polygerrit: 0}));
-      return element._fetchProjects('project', 'pol').then(s => {
-        assert.deepEqual(s[0], {text: 'project:Polygerrit'});
-      });
-    });
-
-    test('Autocomplete doesnt override exact matches to input', () => {
-      sandbox.stub(element.$.restAPI, 'getSuggestedGroups', () =>
-        Promise.resolve({
-          Polygerrit: 0,
-          gerrit: 0,
-          gerrittest: 0,
-        })
-      );
-      return element._fetchGroups('ownerin', 'gerrit').then(s => {
-        assert.deepEqual(s[0], {text: 'ownerin:Polygerrit'});
-        assert.deepEqual(s[1], {text: 'ownerin:gerrit'});
-        assert.deepEqual(s[2], {text: 'ownerin:gerrittest'});
-      });
-    });
-
-    test('Autocompletes accounts with no email', () => {
-      sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () =>
-        Promise.resolve([{name: 'fred'}]));
-      return element._fetchAccounts('owner', 'fr').then(s => {
-        assert.deepEqual(s[0], {text: 'owner:"fred"', label: 'fred'});
-      });
-    });
-
-    test('Autocompletes accounts with email', () => {
-      sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () =>
-        Promise.resolve([{email: 'fred@goog.co'}]));
-      return element._fetchAccounts('owner', 'fr').then(s => {
-        assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: ''});
-      });
+  test('Autocompletes accounts', () => {
+    sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () =>
+      Promise.resolve([
+        {
+          name: 'fred',
+          email: 'fred@goog.co',
+        },
+      ])
+    );
+    return element._fetchAccounts('owner', 'fr').then(s => {
+      assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: 'fred'});
     });
   });
+
+  test('Inserts self as option when valid', () => {
+    sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () =>
+      Promise.resolve([
+        {
+          name: 'fred',
+          email: 'fred@goog.co',
+        },
+      ])
+    );
+    element._fetchAccounts('owner', 's')
+        .then(s => {
+          assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: 'fred'});
+          assert.deepEqual(s[1], {text: 'owner:self'});
+        })
+        .then(() => element._fetchAccounts('owner', 'selfs'))
+        .then(s => {
+          assert.notEqual(s[0], {text: 'owner:self'});
+        });
+  });
+
+  test('Inserts me as option when valid', () => {
+    sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () =>
+      Promise.resolve([
+        {
+          name: 'fred',
+          email: 'fred@goog.co',
+        },
+      ])
+    );
+    return element._fetchAccounts('owner', 'm')
+        .then(s => {
+          assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: 'fred'});
+          assert.deepEqual(s[1], {text: 'owner:me'});
+        })
+        .then(() => element._fetchAccounts('owner', 'meme'))
+        .then(s => {
+          assert.notEqual(s[0], {text: 'owner:me'});
+        });
+  });
+
+  test('Autocompletes groups', () => {
+    sandbox.stub(element.$.restAPI, 'getSuggestedGroups', () =>
+      Promise.resolve({
+        Polygerrit: 0,
+        gerrit: 0,
+        gerrittest: 0,
+      })
+    );
+    return element._fetchGroups('ownerin', 'pol').then(s => {
+      assert.deepEqual(s[0], {text: 'ownerin:Polygerrit'});
+    });
+  });
+
+  test('Autocompletes projects', () => {
+    sandbox.stub(element.$.restAPI, 'getSuggestedProjects', () =>
+      Promise.resolve({Polygerrit: 0}));
+    return element._fetchProjects('project', 'pol').then(s => {
+      assert.deepEqual(s[0], {text: 'project:Polygerrit'});
+    });
+  });
+
+  test('Autocomplete doesnt override exact matches to input', () => {
+    sandbox.stub(element.$.restAPI, 'getSuggestedGroups', () =>
+      Promise.resolve({
+        Polygerrit: 0,
+        gerrit: 0,
+        gerrittest: 0,
+      })
+    );
+    return element._fetchGroups('ownerin', 'gerrit').then(s => {
+      assert.deepEqual(s[0], {text: 'ownerin:Polygerrit'});
+      assert.deepEqual(s[1], {text: 'ownerin:gerrit'});
+      assert.deepEqual(s[2], {text: 'ownerin:gerrittest'});
+    });
+  });
+
+  test('Autocompletes accounts with no email', () => {
+    sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () =>
+      Promise.resolve([{name: 'fred'}]));
+    return element._fetchAccounts('owner', 'fr').then(s => {
+      assert.deepEqual(s[0], {text: 'owner:"fred"', label: 'fred'});
+    });
+  });
+
+  test('Autocompletes accounts with email', () => {
+    sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () =>
+      Promise.resolve([{email: 'fred@goog.co'}]));
+    return element._fetchAccounts('owner', 'fr').then(s => {
+      assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: ''});
+    });
+  });
+});
 </script>