Merge "Make GrRouter not a PolymerElement" into stable-3.6
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts
index 37d5cb0..8ff2460 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts
@@ -16,7 +16,6 @@
  */
 
 import '../../../test/common-test-setup-karma';
-import '../../core/gr-router/gr-router';
 import './gr-change-metadata';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation';
 import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
@@ -63,6 +62,7 @@
 import {ParsedChangeInfo} from '../../../types/types';
 import {GrLinkedChip} from '../../shared/gr-linked-chip/gr-linked-chip';
 import {GrButton} from '../../shared/gr-button/gr-button';
+import {GrRouter} from '../../core/gr-router/gr-router';
 
 const basicFixture = fixtureFromElement('gr-change-metadata');
 
@@ -278,10 +278,10 @@
   });
 
   test('weblinks are visible when other weblinks', () => {
-    const router = document.createElement('gr-router');
+    const router = new GrRouter();
     sinon
       .stub(GerritNav, '_generateWeblinks')
-      .callsFake(router._generateWeblinks.bind(router));
+      .callsFake(router.generateWeblinks.bind(router));
 
     element.commitInfo = {
       ...createCommitInfoWithRequiredCommit(),
@@ -303,10 +303,10 @@
   });
 
   test('weblinks are visible when gitiles and other weblinks', () => {
-    const router = document.createElement('gr-router');
+    const router = new GrRouter();
     sinon
       .stub(GerritNav, '_generateWeblinks')
-      .callsFake(router._generateWeblinks.bind(router));
+      .callsFake(router.generateWeblinks.bind(router));
 
     element.commitInfo = {
       ...createCommitInfoWithRequiredCommit(),
diff --git a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.ts b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.ts
index cb6c9e4..c240a17 100644
--- a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.ts
@@ -16,7 +16,6 @@
  */
 
 import '../../../test/common-test-setup-karma';
-import '../../core/gr-router/gr-router';
 import './gr-commit-info';
 import {GrCommitInfo} from './gr-commit-info';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation';
@@ -26,6 +25,7 @@
   createServerInfo,
 } from '../../../test/test-data-generators';
 import {CommitId, RepoName} from '../../../types/common';
+import {GrRouter} from '../../core/gr-router/gr-router';
 
 const basicFixture = fixtureFromElement('gr-commit-info');
 
@@ -56,10 +56,10 @@
   });
 
   test('use web link when available', () => {
-    const router = document.createElement('gr-router');
+    const router = new GrRouter();
     sinon
       .stub(GerritNav, '_generateWeblinks')
-      .callsFake(router._generateWeblinks.bind(router));
+      .callsFake(router.generateWeblinks.bind(router));
 
     element.change = {...createChange(), labels: {}, project: '' as RepoName};
     element.commitInfo = {
@@ -74,10 +74,10 @@
   });
 
   test('does not relativize web links that begin with scheme', () => {
-    const router = document.createElement('gr-router');
+    const router = new GrRouter();
     sinon
       .stub(GerritNav, '_generateWeblinks')
-      .callsFake(router._generateWeblinks.bind(router));
+      .callsFake(router.generateWeblinks.bind(router));
 
     element.change = {...createChange(), labels: {}, project: '' as RepoName};
     element.commitInfo = {
@@ -92,10 +92,10 @@
   });
 
   test('ignore web links that are neither gitweb nor gitiles', () => {
-    const router = document.createElement('gr-router');
+    const router = new GrRouter();
     sinon
       .stub(GerritNav, '_generateWeblinks')
-      .callsFake(router._generateWeblinks.bind(router));
+      .callsFake(router.generateWeblinks.bind(router));
 
     element.change = {...createChange(), project: 'project-name' as RepoName};
     element.commitInfo = {
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
index fa737c1..f1a4b27 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
@@ -14,13 +14,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {PolymerElement} from '@polymer/polymer/polymer-element';
 import {
   page,
   PageContext,
   PageNextCallback,
 } from '../../../utils/page-wrapper-utils';
-import {htmlTemplate} from './gr-router_html';
 import {
   DashboardSection,
   GeneratedWebLink,
@@ -46,7 +44,6 @@
 } from '../gr-navigation/gr-navigation';
 import {getAppContext} from '../../../services/app-context';
 import {convertToPatchSetNum} from '../../../utils/patch-set-util';
-import {customElement, property} from '@polymer/decorators';
 import {assertNever} from '../../../utils/common-util';
 import {
   BasePatchSetNum,
@@ -288,21 +285,13 @@
   basePatchNum?: BasePatchSetNum;
 }
 
-@customElement('gr-router')
-export class GrRouter extends PolymerElement {
-  static get template() {
-    return htmlTemplate;
-  }
-
-  @property({type: Object})
+export class GrRouter {
   readonly _app = app;
 
-  @property({type: Boolean})
   _isRedirecting?: boolean;
 
   // This variable is to differentiate between internal navigation (false)
   // and for first navigation in app after loaded from server (true).
-  @property({type: Boolean})
   _isInitialLoad = true;
 
   private readonly reporting = getAppContext().reportingService;
@@ -315,19 +304,19 @@
     if (!this._app) {
       return;
     }
-    this._startRouter();
+    this.startRouter();
   }
 
-  _setParams(params: AppElementParams | GenerateUrlParameters) {
+  setParams(params: AppElementParams | GenerateUrlParameters) {
     this.routerModel.updateState({
       view: params.view,
       changeNum: 'changeNum' in params ? params.changeNum : undefined,
       patchNum: 'patchNum' in params ? params.patchNum ?? undefined : undefined,
     });
-    this._appElement().params = params;
+    this.appElement().params = params;
   }
 
-  _appElement(): AppElement {
+  private appElement(): 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
@@ -342,34 +331,34 @@
         .shadowRoot!.getElementById('app-element')!) as AppElement;
   }
 
-  _redirect(url: string) {
+  redirect(url: string) {
     this._isRedirecting = true;
     page.redirect(url);
   }
 
-  _generateUrl(params: GenerateUrlParameters) {
+  generateUrl(params: GenerateUrlParameters) {
     const base = getBaseUrl();
     let url = '';
 
     if (params.view === GerritView.SEARCH) {
-      url = this._generateSearchUrl(params);
+      url = this.generateSearchUrl(params);
     } else if (params.view === GerritView.CHANGE) {
-      url = this._generateChangeUrl(params);
+      url = this.generateChangeUrl(params);
     } else if (params.view === GerritView.DASHBOARD) {
-      url = this._generateDashboardUrl(params);
+      url = this.generateDashboardUrl(params);
     } else if (
       params.view === GerritView.DIFF ||
       params.view === GerritView.EDIT
     ) {
-      url = this._generateDiffOrEditUrl(params);
+      url = this.generateDiffOrEditUrl(params);
     } else if (params.view === GerritView.GROUP) {
-      url = this._generateGroupUrl(params);
+      url = this.generateGroupUrl(params);
     } else if (params.view === GerritView.REPO) {
-      url = this._generateRepoUrl(params);
+      url = this.generateRepoUrl(params);
     } else if (params.view === GerritView.ROOT) {
       url = '/';
     } else if (params.view === GerritView.SETTINGS) {
-      url = this._generateSettingsUrl();
+      url = this.generateSettingsUrl();
     } else {
       assertNever(params, "Can't generate");
     }
@@ -377,33 +366,33 @@
     return base + url;
   }
 
-  _generateWeblinks(
+  generateWeblinks(
     params: GenerateWebLinksParameters
   ): GeneratedWebLink[] | GeneratedWebLink {
     switch (params.type) {
       case WeblinkType.EDIT:
-        return this._getEditWebLinks(params);
+        return this.getEditWebLinks(params);
       case WeblinkType.FILE:
-        return this._getFileWebLinks(params);
+        return this.getFileWebLinks(params);
       case WeblinkType.CHANGE:
-        return this._getChangeWeblinks(params);
+        return this.getChangeWeblinks(params);
       case WeblinkType.PATCHSET:
-        return this._getPatchSetWeblink(params);
+        return this.getPatchSetWeblink(params);
       case WeblinkType.RESOLVE_CONFLICTS:
-        return this._getResolveConflictsWeblinks(params);
+        return this.getResolveConflictsWeblinks(params);
       default:
         // eslint-disable-next-line @typescript-eslint/no-explicit-any
         assertNever(params, `Unsupported weblink ${(params as any).type}!`);
     }
   }
 
-  _getPatchSetWeblink(
+  private getPatchSetWeblink(
     params: GenerateWebLinksPatchsetParameters
   ): GeneratedWebLink {
     const {commit, options} = params;
     const {weblinks, config} = options || {};
     const name = commit && commit.slice(0, 7);
-    const weblink = this._getBrowseCommitWeblink(weblinks, config);
+    const weblink = this.getBrowseCommitWeblink(weblinks, config);
     if (!weblink || !weblink.url) {
       return {name};
     } else {
@@ -411,13 +400,13 @@
     }
   }
 
-  _getResolveConflictsWeblinks(
+  private getResolveConflictsWeblinks(
     params: GenerateWebLinksResolveConflictsParameters
   ): GeneratedWebLink[] {
     return params.options?.weblinks ?? [];
   }
 
-  _firstCodeBrowserWeblink(weblinks: GeneratedWebLink[]) {
+  firstCodeBrowserWeblink(weblinks: GeneratedWebLink[]) {
     // This is an ordered allowed list of web link types that provide direct
     // links to the commit in the url property.
     const codeBrowserLinks = ['gitiles', 'browse', 'gitweb'];
@@ -432,7 +421,7 @@
     return null;
   }
 
-  _getBrowseCommitWeblink(weblinks?: GeneratedWebLink[], config?: ServerInfo) {
+  getBrowseCommitWeblink(weblinks?: GeneratedWebLink[], config?: ServerInfo) {
     if (!weblinks) {
       return null;
     }
@@ -443,7 +432,7 @@
       weblink = weblinks.find(weblink => weblink.name === primaryWeblinkName);
     }
     if (!weblink) {
-      weblink = this._firstCodeBrowserWeblink(weblinks);
+      weblink = this.firstCodeBrowserWeblink(weblinks);
     }
     if (!weblink) {
       return null;
@@ -451,13 +440,13 @@
     return weblink;
   }
 
-  _getChangeWeblinks(
+  getChangeWeblinks(
     params: GenerateWebLinksChangeParameters
   ): GeneratedWebLink[] {
     const weblinks = params.options?.weblinks;
     const config = params.options?.config;
     if (!weblinks || !weblinks.length) return [];
-    const commitWeblink = this._getBrowseCommitWeblink(weblinks, config);
+    const commitWeblink = this.getBrowseCommitWeblink(weblinks, config);
     return weblinks.filter(
       weblink =>
         !commitWeblink ||
@@ -466,15 +455,19 @@
     );
   }
 
-  _getEditWebLinks(params: GenerateWebLinksEditParameters): GeneratedWebLink[] {
+  private getEditWebLinks(
+    params: GenerateWebLinksEditParameters
+  ): GeneratedWebLink[] {
     return params.options?.weblinks ?? [];
   }
 
-  _getFileWebLinks(params: GenerateWebLinksFileParameters): GeneratedWebLink[] {
+  private getFileWebLinks(
+    params: GenerateWebLinksFileParameters
+  ): GeneratedWebLink[] {
     return params.options?.weblinks ?? [];
   }
 
-  _generateSearchUrl(params: GenerateUrlSearchViewParameters) {
+  private generateSearchUrl(params: GenerateUrlSearchViewParameters) {
     let offsetExpr = '';
     if (params.offset && params.offset > 0) {
       offsetExpr = `,${params.offset}`;
@@ -529,8 +522,8 @@
     return '/q/' + operators.join('+') + offsetExpr;
   }
 
-  _generateChangeUrl(params: GenerateUrlChangeViewParameters) {
-    let range = this._getPatchRangeExpression(params);
+  private generateChangeUrl(params: GenerateUrlChangeViewParameters) {
+    let range = this.getPatchRangeExpression(params);
     if (range.length) {
       range = '/' + range;
     }
@@ -559,11 +552,11 @@
     }
   }
 
-  _generateDashboardUrl(params: GenerateUrlDashboardViewParameters) {
+  private generateDashboardUrl(params: GenerateUrlDashboardViewParameters) {
     const repoName = params.repo || params.project || undefined;
     if (params.sections) {
       // Custom dashboard.
-      const queryParams = this._sectionsToEncodedParams(
+      const queryParams = this.sectionsToEncodedParams(
         params.sections,
         repoName
       );
@@ -582,7 +575,10 @@
     }
   }
 
-  _sectionsToEncodedParams(sections: DashboardSection[], repoName?: RepoName) {
+  private sectionsToEncodedParams(
+    sections: DashboardSection[],
+    repoName?: 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.
@@ -593,10 +589,10 @@
     });
   }
 
-  _generateDiffOrEditUrl(
+  private generateDiffOrEditUrl(
     params: GenerateUrlDiffViewParameters | GenerateUrlEditViewParameters
   ) {
-    let range = this._getPatchRangeExpression(params);
+    let range = this.getPatchRangeExpression(params);
     if (range.length) {
       range = '/' + range;
     }
@@ -627,7 +623,7 @@
     }
   }
 
-  _generateGroupUrl(params: GenerateUrlGroupViewParameters) {
+  private generateGroupUrl(params: GenerateUrlGroupViewParameters) {
     let url = `/admin/groups/${encodeURL(`${params.groupId}`, true)}`;
     if (params.detail === GroupDetailView.MEMBERS) {
       url += ',members';
@@ -637,7 +633,7 @@
     return url;
   }
 
-  _generateRepoUrl(params: GenerateUrlRepoViewParameters) {
+  private generateRepoUrl(params: GenerateUrlRepoViewParameters) {
     let url = `/admin/repos/${encodeURL(`${params.repoName}`, true)}`;
     if (params.detail === RepoDetailView.GENERAL) {
       url += ',general';
@@ -655,7 +651,7 @@
     return url;
   }
 
-  _generateSettingsUrl() {
+  private generateSettingsUrl() {
     return '/settings';
   }
 
@@ -664,7 +660,7 @@
    * `basePatchNum` or both, return a string representation of that range. If
    * no range is indicated in the params, the empty string is returned.
    */
-  _getPatchRangeExpression(params: PatchRangeParams) {
+  getPatchRangeExpression(params: PatchRangeParams) {
     let range = '';
     if (params.patchNum) {
       range = `${params.patchNum}`;
@@ -680,7 +676,7 @@
    * modified to fit the proper schema.
    *
    */
-  _normalizePatchRangeParams(params: PatchRangeParams) {
+  normalizePatchRangeParams(params: PatchRangeParams) {
     if (params.basePatchNum === undefined) {
       return false;
     }
@@ -705,7 +701,7 @@
    * Redirect the user to login using the given return-URL for redirection
    * after authentication success.
    */
-  _redirectToLogin(returnUrl: string) {
+  redirectToLogin(returnUrl: string) {
     const basePath = getBaseUrl() || '';
     page('/login/' + encodeURIComponent(returnUrl.substring(basePath.length)));
   }
@@ -717,11 +713,11 @@
    *
    * @return Everything after the first '#' ("a#b#c" -> "b#c").
    */
-  _getHashFromCanonicalPath(canonicalPath: string) {
+  getHashFromCanonicalPath(canonicalPath: string) {
     return canonicalPath.split('#').slice(1).join('#');
   }
 
-  _parseLineAddress(hash: string) {
+  parseLineAddress(hash: string) {
     const match = hash.match(LINE_ADDRESS_PATTERN);
     if (!match) {
       return null;
@@ -740,26 +736,26 @@
    * @return A promise yielding the original route data
    * (if it resolves).
    */
-  _redirectIfNotLoggedIn(data: PageContext) {
+  redirectIfNotLoggedIn(data: PageContext) {
     return this.restApiService.getLoggedIn().then(loggedIn => {
       if (loggedIn) {
         return Promise.resolve();
       } else {
-        this._redirectToLogin(data.canonicalPath);
+        this.redirectToLogin(data.canonicalPath);
         return Promise.reject(new Error());
       }
     });
   }
 
   /**  Page.js middleware that warms the REST API's logged-in cache line. */
-  _loadUserMiddleware(_: PageContext, next: PageNextCallback) {
+  private loadUserMiddleware(_: PageContext, next: PageNextCallback) {
     this.restApiService.getLoggedIn().then(() => {
       next();
     });
   }
 
   /**  Page.js middleware that try parse the querystring into queryMap. */
-  _queryStringMiddleware(ctx: PageContext, next: PageNextCallback) {
+  private queryStringMiddleware(ctx: PageContext, next: PageNextCallback) {
     (ctx as PageContextWithQueryMap).queryMap = this.createQueryMap(ctx);
     next();
   }
@@ -773,7 +769,7 @@
         this.reporting.reportExecution(Execution.REACHABLE_CODE, {
           id: 'noURLSearchParams',
         });
-        return new Map(this._parseQueryString(ctx.querystring));
+        return new Map(this.parseQueryString(ctx.querystring));
       }
     }
     return new Map<string, string>();
@@ -792,34 +788,29 @@
    * 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(
+  mapRoute(
     pattern: string | RegExp,
-    handlerName: keyof GrRouter,
+    handlerName: string,
+    handler: (ctx: PageContextWithQueryMap) => void,
     authRedirect?: boolean
   ) {
-    if (!this[handlerName]) {
-      this.reporting.error(
-        new Error(`Attempted to map route to unknown method: ${handlerName}`)
-      );
-      return;
-    }
     page(
       pattern,
-      (ctx, next) => this._loadUserMiddleware(ctx, next),
-      (ctx, next) => this._queryStringMiddleware(ctx, next),
+      (ctx, next) => this.loadUserMiddleware(ctx, next),
+      (ctx, next) => this.queryStringMiddleware(ctx, next),
       ctx => {
         this.reporting.locationChanged(handlerName);
         const promise = authRedirect
-          ? this._redirectIfNotLoggedIn(ctx)
+          ? this.redirectIfNotLoggedIn(ctx)
           : Promise.resolve();
         promise.then(() => {
-          this[handlerName](ctx as PageContextWithQueryMap);
+          handler(ctx as PageContextWithQueryMap);
         });
       }
     );
   }
 
-  _startRouter() {
+  startRouter() {
     const base = getBaseUrl();
     if (base) {
       page.base(base);
@@ -833,8 +824,8 @@
           page.show(url);
         }
       },
-      params => this._generateUrl(params),
-      params => this._generateWeblinks(params),
+      params => this.generateUrl(params),
+      params => this.generateWeblinks(params),
       x => x
     );
 
@@ -857,7 +848,7 @@
           const usp = searchParams.get('usp');
           this.reporting.reportLifeCycle(LifeCycle.USER_REFERRED_FROM, {usp});
           searchParams.delete('usp');
-          this._redirect(toPath(pathname, searchParams));
+          this.redirect(toPath(pathname, searchParams));
           return;
         }
       }
@@ -872,7 +863,7 @@
         // 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);
+        this.redirect(ctx.hash);
         return;
       }
 
@@ -883,7 +874,7 @@
           hash: window.location.hash,
           pathname: window.location.pathname,
         };
-        this.dispatchEvent(
+        window.dispatchEvent(
           new CustomEvent('location-change', {
             detail,
             composed: true,
@@ -894,225 +885,384 @@
       next();
     });
 
-    this._mapRoute(RoutePattern.ROOT, '_handleRootRoute');
+    this.mapRoute(
+      RoutePattern.ROOT,
+      'handleRootRoute',
+      this.handleRootRoute.bind(this)
+    );
 
-    this._mapRoute(RoutePattern.DASHBOARD, '_handleDashboardRoute');
+    this.mapRoute(
+      RoutePattern.DASHBOARD,
+      'handleDashboardRoute',
+      this.handleDashboardRoute.bind(this)
+    );
 
-    this._mapRoute(
+    this.mapRoute(
       RoutePattern.CUSTOM_DASHBOARD,
-      '_handleCustomDashboardRoute'
+      'handleCustomDashboardRoute',
+      this.handleCustomDashboardRoute.bind(this)
     );
 
-    this._mapRoute(
+    this.mapRoute(
       RoutePattern.PROJECT_DASHBOARD,
-      '_handleProjectDashboardRoute'
+      'handleProjectDashboardRoute',
+      this.handleProjectDashboardRoute
     );
 
-    this._mapRoute(
+    this.mapRoute(
       RoutePattern.LEGACY_PROJECT_DASHBOARD,
-      '_handleLegacyProjectDashboardRoute'
+      'handleLegacyProjectDashboardRoute',
+      this.handleLegacyProjectDashboardRoute.bind(this)
     );
 
-    this._mapRoute(RoutePattern.GROUP_INFO, '_handleGroupInfoRoute', true);
+    this.mapRoute(
+      RoutePattern.GROUP_INFO,
+      'handleGroupInfoRoute',
+      this.handleGroupInfoRoute.bind(this),
+      true
+    );
 
-    this._mapRoute(
+    this.mapRoute(
       RoutePattern.GROUP_AUDIT_LOG,
-      '_handleGroupAuditLogRoute',
+      'handleGroupAuditLogRoute',
+      this.handleGroupAuditLogRoute.bind(this),
       true
     );
 
-    this._mapRoute(
+    this.mapRoute(
       RoutePattern.GROUP_MEMBERS,
-      '_handleGroupMembersRoute',
+      'handleGroupMembersRoute',
+      this.handleGroupMembersRoute.bind(this),
       true
     );
 
-    this._mapRoute(
+    this.mapRoute(
       RoutePattern.GROUP_LIST_OFFSET,
-      '_handleGroupListOffsetRoute',
+      'handleGroupListOffsetRoute',
+      this.handleGroupListOffsetRoute.bind(this),
       true
     );
 
-    this._mapRoute(
+    this.mapRoute(
       RoutePattern.GROUP_LIST_FILTER_OFFSET,
-      '_handleGroupListFilterOffsetRoute',
+      'handleGroupListFilterOffsetRoute',
+      this.handleGroupListFilterOffsetRoute.bind(this),
       true
     );
 
-    this._mapRoute(
+    this.mapRoute(
       RoutePattern.GROUP_LIST_FILTER,
-      '_handleGroupListFilterRoute',
+      'handleGroupListFilterRoute',
+      this.handleGroupListFilterRoute.bind(this),
       true
     );
 
-    this._mapRoute(
+    this.mapRoute(
       RoutePattern.GROUP_SELF,
-      '_handleGroupSelfRedirectRoute',
+      'handleGroupSelfRedirectRoute',
+      this.handleGroupSelfRedirectRoute.bind(this),
       true
     );
 
-    this._mapRoute(RoutePattern.GROUP, '_handleGroupRoute', true);
+    this.mapRoute(
+      RoutePattern.GROUP,
+      'handleGroupRoute',
+      this.handleGroupRoute.bind(this),
+      true
+    );
 
-    this._mapRoute(RoutePattern.PROJECT_OLD, '_handleProjectsOldRoute');
+    this.mapRoute(
+      RoutePattern.PROJECT_OLD,
+      'handleProjectsOldRoute',
+      this.handleProjectsOldRoute.bind(this)
+    );
 
-    this._mapRoute(
+    this.mapRoute(
       RoutePattern.REPO_COMMANDS,
-      '_handleRepoCommandsRoute',
+      'handleRepoCommandsRoute',
+      this.handleRepoCommandsRoute.bind(this),
       true
     );
 
-    this._mapRoute(RoutePattern.REPO_GENERAL, '_handleRepoGeneralRoute');
+    this.mapRoute(
+      RoutePattern.REPO_GENERAL,
+      'handleRepoGeneralRoute',
+      this.handleRepoGeneralRoute.bind(this)
+    );
 
-    this._mapRoute(RoutePattern.REPO_ACCESS, '_handleRepoAccessRoute');
+    this.mapRoute(
+      RoutePattern.REPO_ACCESS,
+      'handleRepoAccessRoute',
+      this.handleRepoAccessRoute.bind(this)
+    );
 
-    this._mapRoute(RoutePattern.REPO_DASHBOARDS, '_handleRepoDashboardsRoute');
+    this.mapRoute(
+      RoutePattern.REPO_DASHBOARDS,
+      'handleRepoDashboardsRoute',
+      this.handleRepoDashboardsRoute.bind(this)
+    );
 
-    this._mapRoute(
+    this.mapRoute(
       RoutePattern.BRANCH_LIST_OFFSET,
-      '_handleBranchListOffsetRoute'
+      'handleBranchListOffsetRoute',
+      this.handleBranchListOffsetRoute.bind(this)
     );
 
-    this._mapRoute(
+    this.mapRoute(
       RoutePattern.BRANCH_LIST_FILTER_OFFSET,
-      '_handleBranchListFilterOffsetRoute'
+      'handleBranchListFilterOffsetRoute',
+      this.handleBranchListFilterOffsetRoute.bind(this)
     );
 
-    this._mapRoute(
+    this.mapRoute(
       RoutePattern.BRANCH_LIST_FILTER,
-      '_handleBranchListFilterRoute'
+      'handleBranchListFilterRoute',
+      this.handleBranchListFilterRoute.bind(this)
     );
 
-    this._mapRoute(RoutePattern.TAG_LIST_OFFSET, '_handleTagListOffsetRoute');
+    this.mapRoute(
+      RoutePattern.TAG_LIST_OFFSET,
+      'handleTagListOffsetRoute',
+      this.handleTagListOffsetRoute.bind(this)
+    );
 
-    this._mapRoute(
+    this.mapRoute(
       RoutePattern.TAG_LIST_FILTER_OFFSET,
-      '_handleTagListFilterOffsetRoute'
+      'handleTagListFilterOffsetRoute',
+      this.handleTagListFilterOffsetRoute.bind(this)
     );
 
-    this._mapRoute(RoutePattern.TAG_LIST_FILTER, '_handleTagListFilterRoute');
+    this.mapRoute(
+      RoutePattern.TAG_LIST_FILTER,
+      'handleTagListFilterRoute',
+      this.handleTagListFilterRoute.bind(this)
+    );
 
-    this._mapRoute(
+    this.mapRoute(
       RoutePattern.LEGACY_CREATE_GROUP,
-      '_handleCreateGroupRoute',
+      'handleCreateGroupRoute',
+      this.handleCreateGroupRoute.bind(this),
       true
     );
 
-    this._mapRoute(
+    this.mapRoute(
       RoutePattern.LEGACY_CREATE_PROJECT,
-      '_handleCreateProjectRoute',
+      'handleCreateProjectRoute',
+      this.handleCreateProjectRoute.bind(this),
       true
     );
 
-    this._mapRoute(RoutePattern.REPO_LIST_OFFSET, '_handleRepoListOffsetRoute');
+    this.mapRoute(
+      RoutePattern.REPO_LIST_OFFSET,
+      'handleRepoListOffsetRoute',
+      this.handleRepoListOffsetRoute.bind(this)
+    );
 
-    this._mapRoute(
+    this.mapRoute(
       RoutePattern.REPO_LIST_FILTER_OFFSET,
-      '_handleRepoListFilterOffsetRoute'
+      'handleRepoListFilterOffsetRoute',
+      this.handleRepoListFilterOffsetRoute.bind(this)
     );
 
-    this._mapRoute(RoutePattern.REPO_LIST_FILTER, '_handleRepoListFilterRoute');
+    this.mapRoute(
+      RoutePattern.REPO_LIST_FILTER,
+      'handleRepoListFilterRoute',
+      this.handleRepoListFilterRoute.bind(this)
+    );
 
-    this._mapRoute(RoutePattern.REPO, '_handleRepoRoute');
+    this.mapRoute(
+      RoutePattern.REPO,
+      'handleRepoRoute',
+      this.handleRepoRoute.bind(this)
+    );
 
-    this._mapRoute(RoutePattern.PLUGINS, '_handlePassThroughRoute');
+    this.mapRoute(
+      RoutePattern.PLUGINS,
+      'handlePassThroughRoute',
+      this.handlePassThroughRoute.bind(this)
+    );
 
-    this._mapRoute(
+    this.mapRoute(
       RoutePattern.PLUGIN_LIST_OFFSET,
-      '_handlePluginListOffsetRoute',
+      'handlePluginListOffsetRoute',
+      this.handlePluginListOffsetRoute.bind(this),
       true
     );
 
-    this._mapRoute(
+    this.mapRoute(
       RoutePattern.PLUGIN_LIST_FILTER_OFFSET,
-      '_handlePluginListFilterOffsetRoute',
+      'handlePluginListFilterOffsetRoute',
+      this.handlePluginListFilterOffsetRoute.bind(this),
       true
     );
 
-    this._mapRoute(
+    this.mapRoute(
       RoutePattern.PLUGIN_LIST_FILTER,
-      '_handlePluginListFilterRoute',
+      'handlePluginListFilterRoute',
+      this.handlePluginListFilterRoute.bind(this),
       true
     );
 
-    this._mapRoute(RoutePattern.PLUGIN_LIST, '_handlePluginListRoute', true);
+    this.mapRoute(
+      RoutePattern.PLUGIN_LIST,
+      'handlePluginListRoute',
+      this.handlePluginListRoute.bind(this),
+      true
+    );
 
-    this._mapRoute(
+    this.mapRoute(
       RoutePattern.QUERY_LEGACY_SUFFIX,
-      '_handleQueryLegacySuffixRoute'
+      'handleQueryLegacySuffixRoute',
+      this.handleQueryLegacySuffixRoute.bind(this)
     );
 
-    this._mapRoute(RoutePattern.QUERY, '_handleQueryRoute');
+    this.mapRoute(
+      RoutePattern.QUERY,
+      'handleQueryRoute',
+      this.handleQueryRoute.bind(this)
+    );
 
-    this._mapRoute(RoutePattern.CHANGE_ID_QUERY, '_handleChangeIdQueryRoute');
+    this.mapRoute(
+      RoutePattern.CHANGE_ID_QUERY,
+      'handleChangeIdQueryRoute',
+      this.handleChangeIdQueryRoute.bind(this)
+    );
 
-    this._mapRoute(RoutePattern.DIFF_LEGACY_LINENUM, '_handleLegacyLinenum');
+    this.mapRoute(
+      RoutePattern.DIFF_LEGACY_LINENUM,
+      'handleLegacyLinenum',
+      this.handleLegacyLinenum.bind(this)
+    );
 
-    this._mapRoute(
+    this.mapRoute(
       RoutePattern.CHANGE_NUMBER_LEGACY,
-      '_handleChangeNumberLegacyRoute'
+      'handleChangeNumberLegacyRoute',
+      this.handleChangeNumberLegacyRoute.bind(this)
     );
 
-    this._mapRoute(RoutePattern.DIFF_EDIT, '_handleDiffEditRoute', true);
+    this.mapRoute(
+      RoutePattern.DIFF_EDIT,
+      'handleDiffEditRoute',
+      this.handleDiffEditRoute.bind(this),
+      true
+    );
 
-    this._mapRoute(RoutePattern.CHANGE_EDIT, '_handleChangeEditRoute', true);
+    this.mapRoute(
+      RoutePattern.CHANGE_EDIT,
+      'handleChangeEditRoute',
+      this.handleChangeEditRoute.bind(this),
+      true
+    );
 
-    this._mapRoute(RoutePattern.COMMENT, '_handleCommentRoute');
+    this.mapRoute(
+      RoutePattern.COMMENT,
+      'handleCommentRoute',
+      this.handleCommentRoute.bind(this)
+    );
 
-    this._mapRoute(RoutePattern.COMMENTS_TAB, '_handleCommentsRoute');
+    this.mapRoute(
+      RoutePattern.COMMENTS_TAB,
+      'handleCommentsRoute',
+      this.handleCommentsRoute.bind(this)
+    );
 
-    this._mapRoute(RoutePattern.DIFF, '_handleDiffRoute');
+    this.mapRoute(
+      RoutePattern.DIFF,
+      'handleDiffRoute',
+      this.handleDiffRoute.bind(this)
+    );
 
-    this._mapRoute(RoutePattern.CHANGE, '_handleChangeRoute');
+    this.mapRoute(
+      RoutePattern.CHANGE,
+      'handleChangeRoute',
+      this.handleChangeRoute.bind(this)
+    );
 
-    this._mapRoute(RoutePattern.CHANGE_LEGACY, '_handleChangeLegacyRoute');
+    this.mapRoute(
+      RoutePattern.CHANGE_LEGACY,
+      'handleChangeLegacyRoute',
+      this.handleChangeLegacyRoute.bind(this)
+    );
 
-    this._mapRoute(RoutePattern.AGREEMENTS, '_handleAgreementsRoute', true);
+    this.mapRoute(
+      RoutePattern.AGREEMENTS,
+      'handleAgreementsRoute',
+      this.handleAgreementsRoute.bind(this),
+      true
+    );
 
-    this._mapRoute(
+    this.mapRoute(
       RoutePattern.NEW_AGREEMENTS,
-      '_handleNewAgreementsRoute',
+      'handleNewAgreementsRoute',
+      this.handleNewAgreementsRoute.bind(this),
       true
     );
 
-    this._mapRoute(
+    this.mapRoute(
       RoutePattern.SETTINGS_LEGACY,
-      '_handleSettingsLegacyRoute',
+      'handleSettingsLegacyRoute',
+      this.handleSettingsLegacyRoute.bind(this),
       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.SETTINGS,
+      'handleSettingsRoute',
+      this.handleSettingsRoute.bind(this),
+      true
     );
 
-    this._mapRoute(RoutePattern.PLUGIN_SCREEN, '_handlePluginScreen');
+    this.mapRoute(
+      RoutePattern.REGISTER,
+      'handleRegisterRoute',
+      this.handleRegisterRoute.bind(this)
+    );
 
-    this._mapRoute(
+    this.mapRoute(
+      RoutePattern.LOG_IN_OR_OUT,
+      'handlePassThroughRoute',
+      this.handlePassThroughRoute.bind(this)
+    );
+
+    this.mapRoute(
+      RoutePattern.IMPROPERLY_ENCODED_PLUS,
+      'handleImproperlyEncodedPlusRoute',
+      this.handleImproperlyEncodedPlusRoute.bind(this)
+    );
+
+    this.mapRoute(
+      RoutePattern.PLUGIN_SCREEN,
+      'handlePluginScreen',
+      this.handlePluginScreen.bind(this)
+    );
+
+    this.mapRoute(
       RoutePattern.DOCUMENTATION_SEARCH_FILTER,
-      '_handleDocumentationSearchRoute'
+      'handleDocumentationSearchRoute',
+      this.handleDocumentationSearchRoute.bind(this)
     );
 
     // redirects /Documentation/q/* to /Documentation/q/filter:*
-    this._mapRoute(
+    this.mapRoute(
       RoutePattern.DOCUMENTATION_SEARCH,
-      '_handleDocumentationSearchRedirectRoute'
+      'handleDocumentationSearchRedirectRoute',
+      this.handleDocumentationSearchRedirectRoute.bind(this)
     );
 
     // Makes sure /Documentation/* links work (doin't return 404)
-    this._mapRoute(
+    this.mapRoute(
       RoutePattern.DOCUMENTATION,
-      '_handleDocumentationRedirectRoute'
+      'handleDocumentationRedirectRoute',
+      this.handleDocumentationRedirectRoute.bind(this)
     );
 
     // Note: this route should appear last so it only catches URLs unmatched
     // by other patterns.
-    this._mapRoute(RoutePattern.DEFAULT, '_handleDefaultRoute');
+    this.mapRoute(
+      RoutePattern.DEFAULT,
+      'handleDefaultRoute',
+      this.handleDefaultRoute.bind(this)
+    );
 
     page.start();
   }
@@ -1121,13 +1271,13 @@
    * @return if handling the route involves asynchrony, then a
    * promise is returned. Otherwise, synchronous handling returns null.
    */
-  _handleRootRoute(data: PageContextWithQueryMap) {
+  handleRootRoute(data: PageContextWithQueryMap) {
     if (data.querystring.match(/^closeAfterLogin/)) {
       // Close child window on redirect after login.
       window.close();
       return null;
     }
-    let hash = this._getHashFromCanonicalPath(data.canonicalPath);
+    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
@@ -1145,14 +1295,14 @@
       if (hash.startsWith('/VE/')) {
         newUrl = base + '/settings' + hash;
       }
-      this._redirect(newUrl);
+      this.redirect(newUrl);
       return null;
     }
     return this.restApiService.getLoggedIn().then(loggedIn => {
       if (loggedIn) {
-        this._redirect('/dashboard/self');
+        this.redirect('/dashboard/self');
       } else {
-        this._redirect('/q/status:open+-is:wip');
+        this.redirect('/q/status:open+-is:wip');
       }
     });
   }
@@ -1163,7 +1313,7 @@
    * @param qs The application/x-www-form-urlencoded string.
    * @return The decoded string.
    */
-  _decodeQueryString(qs: string) {
+  private decodeQueryString(qs: string) {
     return decodeURIComponent(qs.replace(PLUS_PATTERN, ' '));
   }
 
@@ -1175,7 +1325,7 @@
    * @return An array of name/value pairs, where each
    * element is a 2-element array.
    */
-  _parseQueryString(qs: string): Array<QueryStringItem> {
+  parseQueryString(qs: string): Array<QueryStringItem> {
     qs = qs.replace(QUESTION_PATTERN, '');
     if (!qs) {
       return [];
@@ -1186,11 +1336,11 @@
       let name;
       let value;
       if (idx < 0) {
-        name = this._decodeQueryString(param);
+        name = this.decodeQueryString(param);
         value = '';
       } else {
-        name = this._decodeQueryString(param.substring(0, idx));
-        value = this._decodeQueryString(param.substring(idx + 1));
+        name = this.decodeQueryString(param.substring(0, idx));
+        value = this.decodeQueryString(param.substring(idx + 1));
       }
       if (name) {
         params.push([name, value]);
@@ -1202,19 +1352,19 @@
   /**
    * Handle dashboard routes. These may be user, or project dashboards.
    */
-  _handleDashboardRoute(data: PageContextWithQueryMap) {
+  handleDashboardRoute(data: PageContextWithQueryMap) {
     // 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.restApiService.getLoggedIn().then(loggedIn => {
       if (!loggedIn) {
         if (data.params[0].toLowerCase() === 'self') {
-          this._redirectToLogin(data.canonicalPath);
+          this.redirectToLogin(data.canonicalPath);
         } else {
-          this._redirect('/q/owner:' + encodeURIComponent(data.params[0]));
+          this.redirect('/q/owner:' + encodeURIComponent(data.params[0]));
         }
       } else {
-        this._setParams({
+        this.setParams({
           view: GerritView.DASHBOARD,
           user: data.params[0],
         });
@@ -1228,11 +1378,11 @@
    * @param qs Optional query string associated with the route.
    * If not given, window.location.search is used. (Used by tests).
    */
-  _handleCustomDashboardRoute(
+  handleCustomDashboardRoute(
     _: PageContextWithQueryMap,
     qs: string = window.location.search
   ) {
-    const queryParams = this._parseQueryString(qs);
+    const queryParams = this.parseQueryString(qs);
     let title = 'Custom Dashboard';
     const titleParam = queryParams.find(
       elem => elem[0].toLowerCase() === 'title'
@@ -1266,7 +1416,7 @@
 
     if (sections.length > 0) {
       // Custom dashboard view.
-      this._setParams({
+      this.setParams({
         view: GerritView.DASHBOARD,
         user: 'self',
         sections,
@@ -1276,13 +1426,13 @@
     }
 
     // Redirect /dashboard/ -> /dashboard/self.
-    this._redirect('/dashboard/self');
+    this.redirect('/dashboard/self');
     return Promise.resolve();
   }
 
-  _handleProjectDashboardRoute(data: PageContextWithQueryMap) {
+  handleProjectDashboardRoute(data: PageContextWithQueryMap) {
     const project = data.params[0] as RepoName;
-    this._setParams({
+    this.setParams({
       view: GerritView.DASHBOARD,
       project,
       dashboard: decodeURIComponent(data.params[1]) as DashboardId,
@@ -1290,43 +1440,43 @@
     this.reporting.setRepoName(project);
   }
 
-  _handleLegacyProjectDashboardRoute(data: PageContextWithQueryMap) {
-    this._redirect('/p/' + data.params[0] + '/+/dashboard/' + data.params[1]);
+  handleLegacyProjectDashboardRoute(data: PageContextWithQueryMap) {
+    this.redirect('/p/' + data.params[0] + '/+/dashboard/' + data.params[1]);
   }
 
-  _handleGroupInfoRoute(data: PageContextWithQueryMap) {
-    this._redirect('/admin/groups/' + encodeURIComponent(data.params[0]));
+  handleGroupInfoRoute(data: PageContextWithQueryMap) {
+    this.redirect('/admin/groups/' + encodeURIComponent(data.params[0]));
   }
 
-  _handleGroupSelfRedirectRoute(_: PageContextWithQueryMap) {
-    this._redirect('/settings/#Groups');
+  handleGroupSelfRedirectRoute(_: PageContextWithQueryMap) {
+    this.redirect('/settings/#Groups');
   }
 
-  _handleGroupRoute(data: PageContextWithQueryMap) {
-    this._setParams({
+  handleGroupRoute(data: PageContextWithQueryMap) {
+    this.setParams({
       view: GerritView.GROUP,
       groupId: data.params[0] as GroupId,
     });
   }
 
-  _handleGroupAuditLogRoute(data: PageContextWithQueryMap) {
-    this._setParams({
+  handleGroupAuditLogRoute(data: PageContextWithQueryMap) {
+    this.setParams({
       view: GerritView.GROUP,
       detail: GroupDetailView.LOG,
       groupId: data.params[0] as GroupId,
     });
   }
 
-  _handleGroupMembersRoute(data: PageContextWithQueryMap) {
-    this._setParams({
+  handleGroupMembersRoute(data: PageContextWithQueryMap) {
+    this.setParams({
       view: GerritView.GROUP,
       detail: GroupDetailView.MEMBERS,
       groupId: data.params[0] as GroupId,
     });
   }
 
-  _handleGroupListOffsetRoute(data: PageContextWithQueryMap) {
-    this._setParams({
+  handleGroupListOffsetRoute(data: PageContextWithQueryMap) {
+    this.setParams({
       view: GerritView.ADMIN,
       adminView: 'gr-admin-group-list',
       offset: data.params[1] || 0,
@@ -1335,8 +1485,8 @@
     });
   }
 
-  _handleGroupListFilterOffsetRoute(data: PageContextWithQueryMap) {
-    this._setParams({
+  handleGroupListFilterOffsetRoute(data: PageContextWithQueryMap) {
+    this.setParams({
       view: GerritView.ADMIN,
       adminView: 'gr-admin-group-list',
       offset: data.params['offset'],
@@ -1344,15 +1494,15 @@
     });
   }
 
-  _handleGroupListFilterRoute(data: PageContextWithQueryMap) {
-    this._setParams({
+  handleGroupListFilterRoute(data: PageContextWithQueryMap) {
+    this.setParams({
       view: GerritView.ADMIN,
       adminView: 'gr-admin-group-list',
       filter: data.params['filter'] || null,
     });
   }
 
-  _handleProjectsOldRoute(data: PageContextWithQueryMap) {
+  handleProjectsOldRoute(data: PageContextWithQueryMap) {
     let params = '';
     if (data.params[1]) {
       params = encodeURIComponent(data.params[1]);
@@ -1361,12 +1511,12 @@
       }
     }
 
-    this._redirect(`/admin/repos/${params}`);
+    this.redirect(`/admin/repos/${params}`);
   }
 
-  _handleRepoCommandsRoute(data: PageContextWithQueryMap) {
+  handleRepoCommandsRoute(data: PageContextWithQueryMap) {
     const repo = data.params[0] as RepoName;
-    this._setParams({
+    this.setParams({
       view: GerritView.REPO,
       detail: RepoDetailView.COMMANDS,
       repo,
@@ -1374,9 +1524,9 @@
     this.reporting.setRepoName(repo);
   }
 
-  _handleRepoGeneralRoute(data: PageContextWithQueryMap) {
+  handleRepoGeneralRoute(data: PageContextWithQueryMap) {
     const repo = data.params[0] as RepoName;
-    this._setParams({
+    this.setParams({
       view: GerritView.REPO,
       detail: RepoDetailView.GENERAL,
       repo,
@@ -1384,9 +1534,9 @@
     this.reporting.setRepoName(repo);
   }
 
-  _handleRepoAccessRoute(data: PageContextWithQueryMap) {
+  handleRepoAccessRoute(data: PageContextWithQueryMap) {
     const repo = data.params[0] as RepoName;
-    this._setParams({
+    this.setParams({
       view: GerritView.REPO,
       detail: RepoDetailView.ACCESS,
       repo,
@@ -1394,9 +1544,9 @@
     this.reporting.setRepoName(repo);
   }
 
-  _handleRepoDashboardsRoute(data: PageContextWithQueryMap) {
+  handleRepoDashboardsRoute(data: PageContextWithQueryMap) {
     const repo = data.params[0] as RepoName;
-    this._setParams({
+    this.setParams({
       view: GerritView.REPO,
       detail: RepoDetailView.DASHBOARDS,
       repo,
@@ -1404,8 +1554,8 @@
     this.reporting.setRepoName(repo);
   }
 
-  _handleBranchListOffsetRoute(data: PageContextWithQueryMap) {
-    this._setParams({
+  handleBranchListOffsetRoute(data: PageContextWithQueryMap) {
+    this.setParams({
       view: GerritView.REPO,
       detail: RepoDetailView.BRANCHES,
       repo: data.params[0] as RepoName,
@@ -1414,8 +1564,8 @@
     });
   }
 
-  _handleBranchListFilterOffsetRoute(data: PageContextWithQueryMap) {
-    this._setParams({
+  handleBranchListFilterOffsetRoute(data: PageContextWithQueryMap) {
+    this.setParams({
       view: GerritView.REPO,
       detail: RepoDetailView.BRANCHES,
       repo: data.params['repo'] as RepoName,
@@ -1424,8 +1574,8 @@
     });
   }
 
-  _handleBranchListFilterRoute(data: PageContextWithQueryMap) {
-    this._setParams({
+  handleBranchListFilterRoute(data: PageContextWithQueryMap) {
+    this.setParams({
       view: GerritView.REPO,
       detail: RepoDetailView.BRANCHES,
       repo: data.params['repo'] as RepoName,
@@ -1433,8 +1583,8 @@
     });
   }
 
-  _handleTagListOffsetRoute(data: PageContextWithQueryMap) {
-    this._setParams({
+  handleTagListOffsetRoute(data: PageContextWithQueryMap) {
+    this.setParams({
       view: GerritView.REPO,
       detail: RepoDetailView.TAGS,
       repo: data.params[0] as RepoName,
@@ -1443,8 +1593,8 @@
     });
   }
 
-  _handleTagListFilterOffsetRoute(data: PageContextWithQueryMap) {
-    this._setParams({
+  handleTagListFilterOffsetRoute(data: PageContextWithQueryMap) {
+    this.setParams({
       view: GerritView.REPO,
       detail: RepoDetailView.TAGS,
       repo: data.params['repo'] as RepoName,
@@ -1453,8 +1603,8 @@
     });
   }
 
-  _handleTagListFilterRoute(data: PageContextWithQueryMap) {
-    this._setParams({
+  handleTagListFilterRoute(data: PageContextWithQueryMap) {
+    this.setParams({
       view: GerritView.REPO,
       detail: RepoDetailView.TAGS,
       repo: data.params['repo'] as RepoName,
@@ -1462,8 +1612,8 @@
     });
   }
 
-  _handleRepoListOffsetRoute(data: PageContextWithQueryMap) {
-    this._setParams({
+  handleRepoListOffsetRoute(data: PageContextWithQueryMap) {
+    this.setParams({
       view: GerritView.ADMIN,
       adminView: 'gr-repo-list',
       offset: data.params[1] || 0,
@@ -1472,8 +1622,8 @@
     });
   }
 
-  _handleRepoListFilterOffsetRoute(data: PageContextWithQueryMap) {
-    this._setParams({
+  handleRepoListFilterOffsetRoute(data: PageContextWithQueryMap) {
+    this.setParams({
       view: GerritView.ADMIN,
       adminView: 'gr-repo-list',
       offset: data.params['offset'],
@@ -1481,32 +1631,32 @@
     });
   }
 
-  _handleRepoListFilterRoute(data: PageContextWithQueryMap) {
-    this._setParams({
+  handleRepoListFilterRoute(data: PageContextWithQueryMap) {
+    this.setParams({
       view: GerritView.ADMIN,
       adminView: 'gr-repo-list',
       filter: data.params['filter'] || null,
     });
   }
 
-  _handleCreateProjectRoute(_: PageContextWithQueryMap) {
+  handleCreateProjectRoute(_: PageContextWithQueryMap) {
     // Redirects the legacy route to the new route, which displays the project
     // list with a hash 'create'.
-    this._redirect('/admin/repos#create');
+    this.redirect('/admin/repos#create');
   }
 
-  _handleCreateGroupRoute(_: PageContextWithQueryMap) {
+  handleCreateGroupRoute(_: PageContextWithQueryMap) {
     // Redirects the legacy route to the new route, which displays the group
     // list with a hash 'create'.
-    this._redirect('/admin/groups#create');
+    this.redirect('/admin/groups#create');
   }
 
-  _handleRepoRoute(data: PageContextWithQueryMap) {
-    this._redirect(data.path + ',general');
+  handleRepoRoute(data: PageContextWithQueryMap) {
+    this.redirect(data.path + ',general');
   }
 
-  _handlePluginListOffsetRoute(data: PageContextWithQueryMap) {
-    this._setParams({
+  handlePluginListOffsetRoute(data: PageContextWithQueryMap) {
+    this.setParams({
       view: GerritView.ADMIN,
       adminView: 'gr-plugin-list',
       offset: data.params[1] || 0,
@@ -1514,8 +1664,8 @@
     });
   }
 
-  _handlePluginListFilterOffsetRoute(data: PageContextWithQueryMap) {
-    this._setParams({
+  handlePluginListFilterOffsetRoute(data: PageContextWithQueryMap) {
+    this.setParams({
       view: GerritView.ADMIN,
       adminView: 'gr-plugin-list',
       offset: data.params['offset'],
@@ -1523,48 +1673,48 @@
     });
   }
 
-  _handlePluginListFilterRoute(data: PageContextWithQueryMap) {
-    this._setParams({
+  handlePluginListFilterRoute(data: PageContextWithQueryMap) {
+    this.setParams({
       view: GerritView.ADMIN,
       adminView: 'gr-plugin-list',
       filter: data.params['filter'] || null,
     });
   }
 
-  _handlePluginListRoute(_: PageContextWithQueryMap) {
-    this._setParams({
+  handlePluginListRoute(_: PageContextWithQueryMap) {
+    this.setParams({
       view: GerritView.ADMIN,
       adminView: 'gr-plugin-list',
     });
   }
 
-  _handleQueryRoute(data: PageContextWithQueryMap) {
-    this._setParams({
+  handleQueryRoute(data: PageContextWithQueryMap) {
+    this.setParams({
       view: GerritView.SEARCH,
       query: data.params[0],
       offset: data.params[2],
     });
   }
 
-  _handleChangeIdQueryRoute(data: PageContextWithQueryMap) {
+  handleChangeIdQueryRoute(data: PageContextWithQueryMap) {
     // TODO(pcc): This will need to indicate that this was a change ID query if
     // standard queries gain the ability to search places like commit messages
     // for change IDs.
-    this._setParams({
+    this.setParams({
       view: GerritNav.View.SEARCH,
       query: data.params[0],
     });
   }
 
-  _handleQueryLegacySuffixRoute(ctx: PageContextWithQueryMap) {
-    this._redirect(ctx.path.replace(LEGACY_QUERY_SUFFIX_PATTERN, ''));
+  handleQueryLegacySuffixRoute(ctx: PageContextWithQueryMap) {
+    this.redirect(ctx.path.replace(LEGACY_QUERY_SUFFIX_PATTERN, ''));
   }
 
-  _handleChangeNumberLegacyRoute(ctx: PageContextWithQueryMap) {
-    this._redirect('/c/' + encodeURIComponent(ctx.params[0]));
+  handleChangeNumberLegacyRoute(ctx: PageContextWithQueryMap) {
+    this.redirect('/c/' + encodeURIComponent(ctx.params[0]));
   }
 
-  _handleChangeRoute(ctx: PageContextWithQueryMap) {
+  handleChangeRoute(ctx: PageContextWithQueryMap) {
     // Parameter order is based on the regex group number matched.
     const changeNum = Number(ctx.params[1]) as NumericChangeId;
     const params: GenerateUrlChangeViewParameters = {
@@ -1600,10 +1750,10 @@
 
     this.reporting.setRepoName(params.project);
     this.reporting.setChangeId(changeNum);
-    this._redirectOrNavigate(params);
+    this.redirectOrNavigate(params);
   }
 
-  _handleCommentRoute(ctx: PageContextWithQueryMap) {
+  handleCommentRoute(ctx: PageContextWithQueryMap) {
     const changeNum = Number(ctx.params[1]) as NumericChangeId;
     const params: GenerateUrlDiffViewParameters = {
       project: ctx.params[0] as RepoName,
@@ -1614,10 +1764,10 @@
     };
     this.reporting.setRepoName(params.project);
     this.reporting.setChangeId(changeNum);
-    this._redirectOrNavigate(params);
+    this.redirectOrNavigate(params);
   }
 
-  _handleCommentsRoute(ctx: PageContextWithQueryMap) {
+  handleCommentsRoute(ctx: PageContextWithQueryMap) {
     const changeNum = Number(ctx.params[1]) as NumericChangeId;
     const params: GenerateUrlChangeViewParameters = {
       project: ctx.params[0] as RepoName,
@@ -1627,10 +1777,10 @@
     };
     this.reporting.setRepoName(params.project);
     this.reporting.setChangeId(changeNum);
-    this._redirectOrNavigate(params);
+    this.redirectOrNavigate(params);
   }
 
-  _handleDiffRoute(ctx: PageContextWithQueryMap) {
+  handleDiffRoute(ctx: PageContextWithQueryMap) {
     const changeNum = Number(ctx.params[1]) as NumericChangeId;
     // Parameter order is based on the regex group number matched.
     const params: GenerateUrlDiffViewParameters = {
@@ -1641,42 +1791,42 @@
       path: ctx.params[8],
       view: GerritView.DIFF,
     };
-    const address = this._parseLineAddress(ctx.hash);
+    const address = this.parseLineAddress(ctx.hash);
     if (address) {
       params.leftSide = address.leftSide;
       params.lineNum = address.lineNum;
     }
     this.reporting.setRepoName(params.project);
     this.reporting.setChangeId(changeNum);
-    this._redirectOrNavigate(params);
+    this.redirectOrNavigate(params);
   }
 
-  _handleChangeLegacyRoute(ctx: PageContextWithQueryMap) {
+  handleChangeLegacyRoute(ctx: PageContextWithQueryMap) {
     const changeNum = Number(ctx.params[0]) as NumericChangeId;
     if (!changeNum) {
-      this._show404();
+      this.show404();
       return;
     }
     this.restApiService.getFromProjectLookup(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();
+        this.show404();
         return;
       }
-      this._redirect(`/c/${project}/+/${changeNum}/${ctx.params[1]}`);
+      this.redirect(`/c/${project}/+/${changeNum}/${ctx.params[1]}`);
     });
   }
 
-  _handleLegacyLinenum(ctx: PageContextWithQueryMap) {
-    this._redirect(ctx.path.replace(LEGACY_LINENUM_PATTERN, '#$1'));
+  handleLegacyLinenum(ctx: PageContextWithQueryMap) {
+    this.redirect(ctx.path.replace(LEGACY_LINENUM_PATTERN, '#$1'));
   }
 
-  _handleDiffEditRoute(ctx: PageContextWithQueryMap) {
+  handleDiffEditRoute(ctx: PageContextWithQueryMap) {
     // Parameter order is based on the regex group number matched.
     const project = ctx.params[0] as RepoName;
     const changeNum = Number(ctx.params[1]) as NumericChangeId;
-    this._redirectOrNavigate({
+    this.redirectOrNavigate({
       project,
       changeNum,
       // for edit view params, patchNum cannot be undefined
@@ -1689,7 +1839,7 @@
     this.reporting.setChangeId(changeNum);
   }
 
-  _handleChangeEditRoute(ctx: PageContextWithQueryMap) {
+  handleChangeEditRoute(ctx: PageContextWithQueryMap) {
     // Parameter order is based on the regex group number matched.
     const project = ctx.params[0] as RepoName;
     const changeNum = Number(ctx.params[1]) as NumericChangeId;
@@ -1709,7 +1859,7 @@
         location.href.replace(/[?&]forceReload=true/, '')
       );
     }
-    this._redirectOrNavigate(params);
+    this.redirectOrNavigate(params);
 
     this.reporting.setRepoName(project);
     this.reporting.setChangeId(changeNum);
@@ -1719,42 +1869,42 @@
    * Normalize the patch range params for a the change or diff view and
    * redirect if URL upgrade is needed.
    */
-  _redirectOrNavigate(params: GenerateUrlParameters & PatchRangeParams) {
-    const needsRedirect = this._normalizePatchRangeParams(params);
+  private redirectOrNavigate(params: GenerateUrlParameters & PatchRangeParams) {
+    const needsRedirect = this.normalizePatchRangeParams(params);
     if (needsRedirect) {
-      this._redirect(this._generateUrl(params));
+      this.redirect(this.generateUrl(params));
     } else {
-      this._setParams(params);
+      this.setParams(params);
     }
   }
 
-  _handleAgreementsRoute() {
-    this._redirect('/settings/#Agreements');
+  handleAgreementsRoute() {
+    this.redirect('/settings/#Agreements');
   }
 
-  _handleNewAgreementsRoute(data: PageContextWithQueryMap) {
+  handleNewAgreementsRoute(data: PageContextWithQueryMap) {
     data.params['view'] = GerritView.AGREEMENTS;
     // TODO(TS): create valid object
-    this._setParams(data.params as unknown as AppElementAgreementParam);
+    this.setParams(data.params as unknown as AppElementAgreementParam);
   }
 
-  _handleSettingsLegacyRoute(data: PageContextWithQueryMap) {
+  handleSettingsLegacyRoute(data: PageContextWithQueryMap) {
     // 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({
+    this.setParams({
       view: GerritView.SETTINGS,
       emailToken: token,
     });
   }
 
-  _handleSettingsRoute(_: PageContextWithQueryMap) {
-    this._setParams({view: GerritView.SETTINGS});
+  handleSettingsRoute(_: PageContextWithQueryMap) {
+    this.setParams({view: GerritView.SETTINGS});
   }
 
-  _handleRegisterRoute(ctx: PageContextWithQueryMap) {
-    this._setParams({justRegistered: true});
+  handleRegisterRoute(ctx: PageContextWithQueryMap) {
+    this.setParams({justRegistered: true});
     let path = ctx.params[0] || '/';
 
     // Prevent redirect looping.
@@ -1765,14 +1915,14 @@
     if (path[0] !== '/') {
       return;
     }
-    this._redirect(getBaseUrl() + path);
+    this.redirect(getBaseUrl() + path);
   }
 
   /**
    * Handler for routes that should pass through the router and not be caught
    * by the catchall _handleDefaultRoute handler.
    */
-  _handlePassThroughRoute() {
+  handlePassThroughRoute() {
     windowLocationReload();
   }
 
@@ -1780,66 +1930,60 @@
    * URL may sometimes have /+/ encoded to / /.
    * Context: Issue 6888, Issue 7100
    */
-  _handleImproperlyEncodedPlusRoute(ctx: PageContextWithQueryMap) {
-    let hash = this._getHashFromCanonicalPath(ctx.canonicalPath);
+  handleImproperlyEncodedPlusRoute(ctx: PageContextWithQueryMap) {
+    let hash = this.getHashFromCanonicalPath(ctx.canonicalPath);
     if (hash.length) {
       hash = '#' + hash;
     }
-    this._redirect(`/c/${ctx.params[0]}/+/${ctx.params[1]}${hash}`);
+    this.redirect(`/c/${ctx.params[0]}/+/${ctx.params[1]}${hash}`);
   }
 
-  _handlePluginScreen(ctx: PageContextWithQueryMap) {
+  handlePluginScreen(ctx: PageContextWithQueryMap) {
     const view = GerritView.PLUGIN_SCREEN;
     const plugin = ctx.params[0];
     const screen = ctx.params[1];
-    this._setParams({view, plugin, screen});
+    this.setParams({view, plugin, screen});
   }
 
-  _handleDocumentationSearchRoute(data: PageContextWithQueryMap) {
-    this._setParams({
+  handleDocumentationSearchRoute(data: PageContextWithQueryMap) {
+    this.setParams({
       view: GerritView.DOCUMENTATION_SEARCH,
       filter: data.params['filter'] || null,
     });
   }
 
-  _handleDocumentationSearchRedirectRoute(data: PageContextWithQueryMap) {
-    this._redirect(
+  handleDocumentationSearchRedirectRoute(data: PageContextWithQueryMap) {
+    this.redirect(
       '/Documentation/q/filter:' + encodeURIComponent(data.params[0])
     );
   }
 
-  _handleDocumentationRedirectRoute(data: PageContextWithQueryMap) {
+  handleDocumentationRedirectRoute(data: PageContextWithQueryMap) {
     if (data.params[1]) {
       windowLocationReload();
     } else {
       // Redirect /Documentation to /Documentation/index.html
-      this._redirect('/Documentation/index.html');
+      this.redirect('/Documentation/index.html');
     }
   }
 
   /**
    * Catchall route for when no other route is matched.
    */
-  _handleDefaultRoute() {
+  handleDefaultRoute() {
     if (this._isInitialLoad) {
       // Server recognized this route as polygerrit, so we show 404.
-      this._show404();
+      this.show404();
     } else {
       // Route can be recognized by server, so we pass it to server.
-      this._handlePassThroughRoute();
+      this.handlePassThroughRoute();
     }
   }
 
-  _show404() {
+  private 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.
     firePageError(new Response('', {status: 404}));
   }
 }
-
-declare global {
-  interface HTMLElementTagNameMap {
-    'gr-router': GrRouter;
-  }
-}
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_html.ts b/polygerrit-ui/app/elements/core/gr-router/gr-router_html.ts
deleted file mode 100644
index 1489006..0000000
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router_html.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html``;
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts
index 0b9921c..4c147bb 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts
@@ -63,18 +63,16 @@
 } from '../../../test/test-data-generators';
 import {AppElementParams} from '../../gr-app-types';
 
-const basicFixture = fixtureFromElement('gr-router');
-
 suite('gr-router tests', () => {
-  let element: GrRouter;
+  let router: GrRouter;
 
   setup(() => {
-    element = basicFixture.instantiate();
+    router = new GrRouter();
   });
 
-  test('_firstCodeBrowserWeblink', () => {
+  test('firstCodeBrowserWeblink', () => {
     assert.deepEqual(
-      element._firstCodeBrowserWeblink([
+      router.firstCodeBrowserWeblink([
         {name: 'gitweb'},
         {name: 'gitiles'},
         {name: 'browse'},
@@ -84,12 +82,12 @@
     );
 
     assert.deepEqual(
-      element._firstCodeBrowserWeblink([{name: 'gitweb'}, {name: 'test'}]),
+      router.firstCodeBrowserWeblink([{name: 'gitweb'}, {name: 'test'}]),
       {name: 'gitweb'}
     );
   });
 
-  test('_getBrowseCommitWeblink', () => {
+  test('getBrowseCommitWeblink', () => {
     const browserLink = {name: 'browser', url: 'browser/url'};
     const link = {name: 'test', url: 'test/url'};
     const weblinks = [browserLink, link];
@@ -97,17 +95,17 @@
       ...createServerInfo(),
       gerrit: {...createGerritInfo(), primary_weblink_name: browserLink.name},
     };
-    sinon.stub(element, '_firstCodeBrowserWeblink').returns(link);
+    sinon.stub(router, 'firstCodeBrowserWeblink').returns(link);
 
     assert.deepEqual(
-      element._getBrowseCommitWeblink(weblinks, config),
+      router.getBrowseCommitWeblink(weblinks, config),
       browserLink
     );
 
-    assert.deepEqual(element._getBrowseCommitWeblink(weblinks), link);
+    assert.deepEqual(router.getBrowseCommitWeblink(weblinks), link);
   });
 
-  test('_getChangeWeblinks', () => {
+  test('getChangeWeblinks', () => {
     const link = {name: 'test', url: 'test/url'};
     const browserLink = {name: 'browser', url: 'browser/url'};
     const mapLinksToConfig = (weblinks: WebLinkInfo[]) => {
@@ -118,81 +116,81 @@
         options: {weblinks},
       };
     };
-    sinon.stub(element, '_getBrowseCommitWeblink').returns(browserLink);
+    sinon.stub(router, 'getBrowseCommitWeblink').returns(browserLink);
 
     assert.deepEqual(
-      element._getChangeWeblinks(mapLinksToConfig([link, browserLink]))[0],
+      router.getChangeWeblinks(mapLinksToConfig([link, browserLink]))[0],
       {name: 'test', url: 'test/url'}
     );
 
-    assert.deepEqual(element._getChangeWeblinks(mapLinksToConfig([link]))[0], {
+    assert.deepEqual(router.getChangeWeblinks(mapLinksToConfig([link]))[0], {
       name: 'test',
       url: 'test/url',
     });
 
     link.url = `https://${link.url}`;
-    assert.deepEqual(element._getChangeWeblinks(mapLinksToConfig([link]))[0], {
+    assert.deepEqual(router.getChangeWeblinks(mapLinksToConfig([link]))[0], {
       name: 'test',
       url: 'https://test/url',
     });
   });
 
-  test('_getHashFromCanonicalPath', () => {
+  test('getHashFromCanonicalPath', () => {
     let url = '/foo/bar';
-    let hash = element._getHashFromCanonicalPath(url);
+    let hash = router.getHashFromCanonicalPath(url);
     assert.equal(hash, '');
 
     url = '';
-    hash = element._getHashFromCanonicalPath(url);
+    hash = router.getHashFromCanonicalPath(url);
     assert.equal(hash, '');
 
     url = '/foo#bar';
-    hash = element._getHashFromCanonicalPath(url);
+    hash = router.getHashFromCanonicalPath(url);
     assert.equal(hash, 'bar');
 
     url = '/foo#bar#baz';
-    hash = element._getHashFromCanonicalPath(url);
+    hash = router.getHashFromCanonicalPath(url);
     assert.equal(hash, 'bar#baz');
 
     url = '#foo#bar#baz';
-    hash = element._getHashFromCanonicalPath(url);
+    hash = router.getHashFromCanonicalPath(url);
     assert.equal(hash, 'foo#bar#baz');
   });
 
-  suite('_parseLineAddress', () => {
+  suite('parseLineAddress', () => {
     test('returns null for empty and invalid hashes', () => {
-      let actual = element._parseLineAddress('');
+      let actual = router.parseLineAddress('');
       assert.isNull(actual);
 
-      actual = element._parseLineAddress('foobar');
+      actual = router.parseLineAddress('foobar');
       assert.isNull(actual);
 
-      actual = element._parseLineAddress('foo123');
+      actual = router.parseLineAddress('foo123');
       assert.isNull(actual);
 
-      actual = element._parseLineAddress('123bar');
+      actual = router.parseLineAddress('123bar');
       assert.isNull(actual);
     });
 
     test('parses correctly', () => {
-      let actual = element._parseLineAddress('1234');
+      let actual = router.parseLineAddress('1234');
       assert.isOk(actual);
       assert.equal(actual!.lineNum, 1234);
       assert.isFalse(actual!.leftSide);
 
-      actual = element._parseLineAddress('a4');
+      actual = router.parseLineAddress('a4');
       assert.isOk(actual);
       assert.equal(actual!.lineNum, 4);
       assert.isTrue(actual!.leftSide);
 
-      actual = element._parseLineAddress('b77');
+      actual = router.parseLineAddress('b77');
       assert.isOk(actual);
       assert.equal(actual!.lineNum, 77);
       assert.isTrue(actual!.leftSide);
     });
   });
 
-  test('_startRouter requires auth for the right handlers', () => {
+  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.
 
@@ -202,15 +200,15 @@
     sinon.stub(page, 'start');
     sinon.stub(page, 'base');
     sinon
-      .stub(element, '_mapRoute')
-      .callsFake((_pattern, methodName, usesAuth) => {
+      .stub(router, 'mapRoute')
+      .callsFake((_pattern, methodName, _method, usesAuth) => {
         if (usesAuth) {
           requiresAuth[methodName] = true;
         } else {
           doesNotRequireAuth[methodName] = true;
         }
       });
-    element._startRouter();
+    router.startRouter();
 
     const actualRequiresAuth = Object.keys(requiresAuth);
     actualRequiresAuth.sort();
@@ -218,73 +216,73 @@
     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',
+      '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',
-      '_handleChangeIdQueryRoute',
-      '_handleChangeNumberLegacyRoute',
-      '_handleChangeRoute',
-      '_handleCommentRoute',
-      '_handleCommentsRoute',
-      '_handleDiffRoute',
-      '_handleDefaultRoute',
-      '_handleChangeLegacyRoute',
-      '_handleDocumentationRedirectRoute',
-      '_handleDocumentationSearchRoute',
-      '_handleDocumentationSearchRedirectRoute',
-      '_handleLegacyLinenum',
-      '_handleImproperlyEncodedPlusRoute',
-      '_handlePassThroughRoute',
-      '_handleProjectDashboardRoute',
-      '_handleLegacyProjectDashboardRoute',
-      '_handleProjectsOldRoute',
-      '_handleRepoAccessRoute',
-      '_handleRepoDashboardsRoute',
-      '_handleRepoGeneralRoute',
-      '_handleRepoListFilterOffsetRoute',
-      '_handleRepoListFilterRoute',
-      '_handleRepoListOffsetRoute',
-      '_handleRepoRoute',
-      '_handleQueryLegacySuffixRoute',
-      '_handleQueryRoute',
-      '_handleRegisterRoute',
-      '_handleTagListFilterOffsetRoute',
-      '_handleTagListFilterRoute',
-      '_handleTagListOffsetRoute',
-      '_handlePluginScreen',
+      'handleBranchListFilterOffsetRoute',
+      'handleBranchListFilterRoute',
+      'handleBranchListOffsetRoute',
+      'handleChangeIdQueryRoute',
+      'handleChangeNumberLegacyRoute',
+      'handleChangeRoute',
+      'handleCommentRoute',
+      'handleCommentsRoute',
+      'handleDiffRoute',
+      'handleDefaultRoute',
+      'handleChangeLegacyRoute',
+      'handleDocumentationRedirectRoute',
+      'handleDocumentationSearchRoute',
+      'handleDocumentationSearchRedirectRoute',
+      'handleLegacyLinenum',
+      'handleImproperlyEncodedPlusRoute',
+      'handlePassThroughRoute',
+      'handleProjectDashboardRoute',
+      'handleLegacyProjectDashboardRoute',
+      'handleProjectsOldRoute',
+      'handleRepoAccessRoute',
+      'handleRepoDashboardsRoute',
+      'handleRepoGeneralRoute',
+      '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',
+      'handleDashboardRoute',
+      'handleCustomDashboardRoute',
+      'handleRootRoute',
     ];
 
     const shouldNotRequireAuth = unauthenticatedHandlers.concat(
@@ -294,7 +292,7 @@
     assert.deepEqual(actualDoesNotRequireAuth, shouldNotRequireAuth);
   });
 
-  test('_redirectIfNotLoggedIn while logged in', () => {
+  test('redirectIfNotLoggedIn while logged in', () => {
     stubRestApi('getLoggedIn').returns(Promise.resolve(true));
     const data = {
       save() {},
@@ -308,15 +306,15 @@
       hash: '',
       params: {test: 'test'},
     };
-    const redirectStub = sinon.stub(element, '_redirectToLogin');
-    return element._redirectIfNotLoggedIn(data).then(() => {
+    const redirectStub = sinon.stub(router, 'redirectToLogin');
+    return router.redirectIfNotLoggedIn(data).then(() => {
       assert.isFalse(redirectStub.called);
     });
   });
 
-  test('_redirectIfNotLoggedIn while logged out', () => {
+  test('redirectIfNotLoggedIn while logged out', () => {
     stubRestApi('getLoggedIn').returns(Promise.resolve(false));
-    const redirectStub = sinon.stub(element, '_redirectToLogin');
+    const redirectStub = sinon.stub(router, 'redirectToLogin');
     const data = {
       save() {},
       handled: true,
@@ -330,8 +328,8 @@
       params: {test: 'test'},
     };
     return new Promise(resolve => {
-      element
-        ._redirectIfNotLoggedIn(data)
+      router
+        .redirectIfNotLoggedIn(data)
         .then(() => {
           assert.isTrue(false, 'Should never execute');
         })
@@ -353,14 +351,14 @@
         statuses: ['op%en'],
       };
       assert.equal(
-        element._generateUrl(params),
+        router.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),
+        router.generateUrl(params),
         '/q/owner:a%2525b+project:c%2525d+branch:e%2525f+' +
           'topic:g%2525h+status:op%2525en,100'
       );
@@ -368,17 +366,17 @@
 
       // The presence of the query param overrides other params.
       params.query = 'foo$bar';
-      assert.equal(element._generateUrl(params), '/q/foo%2524bar');
+      assert.equal(router.generateUrl(params), '/q/foo%2524bar');
 
       params.offset = 100;
-      assert.equal(element._generateUrl(params), '/q/foo%2524bar,100');
+      assert.equal(router.generateUrl(params), '/q/foo%2524bar,100');
 
       params = {
         view: GerritNav.View.SEARCH,
         statuses: ['a', 'b', 'c'],
       };
       assert.equal(
-        element._generateUrl(params),
+        router.generateUrl(params),
         '/q/(status:a OR status:b OR status:c)'
       );
 
@@ -386,17 +384,17 @@
         view: GerritNav.View.SEARCH,
         topic: 'test' as TopicName,
       };
-      assert.equal(element._generateUrl(params), '/q/topic:test');
+      assert.equal(router.generateUrl(params), '/q/topic:test');
       params = {
         view: GerritNav.View.SEARCH,
         topic: 'test test' as TopicName,
       };
-      assert.equal(element._generateUrl(params), '/q/topic:"test+test"');
+      assert.equal(router.generateUrl(params), '/q/topic:"test+test"');
       params = {
         view: GerritNav.View.SEARCH,
         topic: 'test:test' as TopicName,
       };
-      assert.equal(element._generateUrl(params), '/q/topic:"test:test"');
+      assert.equal(router.generateUrl(params), '/q/topic:"test:test"');
     });
 
     test('change', () => {
@@ -406,16 +404,16 @@
         project: 'test' as RepoName,
       };
 
-      assert.equal(element._generateUrl(params), '/c/test/+/1234');
+      assert.equal(router.generateUrl(params), '/c/test/+/1234');
 
       params.patchNum = 10 as PatchSetNum;
-      assert.equal(element._generateUrl(params), '/c/test/+/1234/10');
+      assert.equal(router.generateUrl(params), '/c/test/+/1234/10');
 
       params.basePatchNum = 5 as BasePatchSetNum;
-      assert.equal(element._generateUrl(params), '/c/test/+/1234/5..10');
+      assert.equal(router.generateUrl(params), '/c/test/+/1234/5..10');
 
       params.messageHash = '#123';
-      assert.equal(element._generateUrl(params), '/c/test/+/1234/5..10#123');
+      assert.equal(router.generateUrl(params), '/c/test/+/1234/5..10#123');
     });
 
     test('change with repo name encoding', () => {
@@ -425,7 +423,7 @@
         project: 'x+/y+/z+/w' as RepoName,
       };
       assert.equal(
-        element._generateUrl(params),
+        router.generateUrl(params),
         '/c/x%252B/y%252B/z%252B/w/+/1234'
       );
     });
@@ -438,17 +436,17 @@
         patchNum: 12 as PatchSetNum,
         project: '' as RepoName,
       };
-      assert.equal(element._generateUrl(params), '/c/42/12/x%252By/path.cpp');
+      assert.equal(router.generateUrl(params), '/c/42/12/x%252By/path.cpp');
 
       params.project = 'test' as RepoName;
       assert.equal(
-        element._generateUrl(params),
+        router.generateUrl(params),
         '/c/test/+/42/12/x%252By/path.cpp'
       );
 
       params.basePatchNum = 6 as BasePatchSetNum;
       assert.equal(
-        element._generateUrl(params),
+        router.generateUrl(params),
         '/c/test/+/42/6..12/x%252By/path.cpp'
       );
 
@@ -456,19 +454,16 @@
       params.patchNum = 2 as PatchSetNum;
       delete params.basePatchNum;
       assert.equal(
-        element._generateUrl(params),
+        router.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');
+      assert.equal(router.generateUrl(params), '/c/test/+/42/2/file.cpp#123');
 
       params.leftSide = true;
-      assert.equal(
-        element._generateUrl(params),
-        '/c/test/+/42/2/file.cpp#b123'
-      );
+      assert.equal(router.generateUrl(params), '/c/test/+/42/2/file.cpp#b123');
     });
 
     test('diff with repo name encoding', () => {
@@ -480,7 +475,7 @@
         project: 'x+/y' as RepoName,
       };
       assert.equal(
-        element._generateUrl(params),
+        router.generateUrl(params),
         '/c/x%252B/y/+/42/12/x%252By/path.cpp'
       );
     });
@@ -494,26 +489,26 @@
         patchNum: 'edit' as PatchSetNum,
       };
       assert.equal(
-        element._generateUrl(params),
+        router.generateUrl(params),
         '/c/test/+/42/edit/x%252By/path.cpp,edit'
       );
     });
 
-    test('_getPatchRangeExpression', () => {
+    test('getPatchRangeExpression', () => {
       const params: PatchRangeParams = {};
-      let actual = element._getPatchRangeExpression(params);
+      let actual = router.getPatchRangeExpression(params);
       assert.equal(actual, '');
 
       params.patchNum = 4 as PatchSetNum;
-      actual = element._getPatchRangeExpression(params);
+      actual = router.getPatchRangeExpression(params);
       assert.equal(actual, '4');
 
       params.basePatchNum = 2 as BasePatchSetNum;
-      actual = element._getPatchRangeExpression(params);
+      actual = router.getPatchRangeExpression(params);
       assert.equal(actual, '2..4');
 
       delete params.patchNum;
-      actual = element._getPatchRangeExpression(params);
+      actual = router.getPatchRangeExpression(params);
       assert.equal(actual, '2..');
     });
 
@@ -522,7 +517,7 @@
         const params: GenerateUrlDashboardViewParameters = {
           view: GerritView.DASHBOARD,
         };
-        assert.equal(element._generateUrl(params), '/dashboard/self');
+        assert.equal(router.generateUrl(params), '/dashboard/self');
       });
 
       test('user dashboard', () => {
@@ -530,7 +525,7 @@
           view: GerritView.DASHBOARD,
           user: 'user',
         };
-        assert.equal(element._generateUrl(params), '/dashboard/user');
+        assert.equal(router.generateUrl(params), '/dashboard/user');
       });
 
       test('custom self dashboard, no title', () => {
@@ -542,7 +537,7 @@
           ],
         };
         assert.equal(
-          element._generateUrl(params),
+          router.generateUrl(params),
           '/dashboard/?section%201=query%201&section%202=query%202'
         );
       });
@@ -557,7 +552,7 @@
           repo: 'repo-name' as RepoName,
         };
         assert.equal(
-          element._generateUrl(params),
+          router.generateUrl(params),
           '/dashboard/?section%201=query%201%20repo-name&' +
             'section%202=query%202%20repo-name'
         );
@@ -571,7 +566,7 @@
           title: 'custom dashboard',
         };
         assert.equal(
-          element._generateUrl(params),
+          router.generateUrl(params),
           '/dashboard/user?name=query&title=custom%20dashboard'
         );
       });
@@ -583,7 +578,7 @@
           dashboard: 'default:main' as DashboardId,
         };
         assert.equal(
-          element._generateUrl(params),
+          router.generateUrl(params),
           '/p/gerrit/repo/+/dashboard/default:main'
         );
       });
@@ -595,7 +590,7 @@
           dashboard: 'default:main' as DashboardId,
         };
         assert.equal(
-          element._generateUrl(params),
+          router.generateUrl(params),
           '/p/gerrit/project/+/dashboard/default:main'
         );
       });
@@ -607,7 +602,7 @@
           view: GerritView.GROUP,
           groupId: '1234' as GroupId,
         };
-        assert.equal(element._generateUrl(params), '/admin/groups/1234');
+        assert.equal(router.generateUrl(params), '/admin/groups/1234');
       });
 
       test('group members', () => {
@@ -616,10 +611,7 @@
           groupId: '1234' as GroupId,
           detail: 'members' as GroupDetailView,
         };
-        assert.equal(
-          element._generateUrl(params),
-          '/admin/groups/1234,members'
-        );
+        assert.equal(router.generateUrl(params), '/admin/groups/1234,members');
       });
 
       test('group audit log', () => {
@@ -629,7 +621,7 @@
           detail: 'log' as GroupDetailView,
         };
         assert.equal(
-          element._generateUrl(params),
+          router.generateUrl(params),
           '/admin/groups/1234,audit-log'
         );
       });
@@ -637,13 +629,13 @@
   });
 
   suite('param normalization', () => {
-    suite('_normalizePatchRangeParams', () => {
+    suite('normalizePatchRangeParams', () => {
       test('range n..n normalizes to n', () => {
         const params: PatchRangeParams = {
           basePatchNum: 4 as BasePatchSetNum,
           patchNum: 4 as PatchSetNum,
         };
-        const needsRedirect = element._normalizePatchRangeParams(params);
+        const needsRedirect = router.normalizePatchRangeParams(params);
         assert.isTrue(needsRedirect);
         assert.equal(params.basePatchNum, ParentPatchSetNum);
         assert.equal(params.patchNum, 4 as PatchSetNum);
@@ -651,7 +643,7 @@
 
       test('range n.. normalizes to n', () => {
         const params: PatchRangeParams = {basePatchNum: 4 as BasePatchSetNum};
-        const needsRedirect = element._normalizePatchRangeParams(params);
+        const needsRedirect = router.normalizePatchRangeParams(params);
         assert.isFalse(needsRedirect);
         assert.equal(params.basePatchNum, ParentPatchSetNum);
         assert.equal(params.patchNum, 4 as PatchSetNum);
@@ -672,7 +664,7 @@
       methodName: string,
       params: AppElementParams | GenerateUrlParameters
     ) {
-      (element as any)[methodName](data);
+      (router as any)[methodName](data);
       assert.deepEqual(setParamsStub.lastCall.args[0], params);
     }
 
@@ -693,17 +685,17 @@
     }
 
     setup(() => {
-      redirectStub = sinon.stub(element, '_redirect');
-      setParamsStub = sinon.stub(element, '_setParams');
-      handlePassThroughRoute = sinon.stub(element, '_handlePassThroughRoute');
+      redirectStub = sinon.stub(router, 'redirect');
+      setParamsStub = sinon.stub(router, 'setParams');
+      handlePassThroughRoute = sinon.stub(router, 'handlePassThroughRoute');
     });
 
-    test('_handleLegacyProjectDashboardRoute', () => {
+    test('handleLegacyProjectDashboardRoute', () => {
       const params = {
         ...createPageContext(),
         params: {0: 'gerrit/project', 1: 'dashboard:main'},
       };
-      element._handleLegacyProjectDashboardRoute(params);
+      router.handleLegacyProjectDashboardRoute(params);
       assert.isTrue(redirectStub.calledOnce);
       assert.equal(
         redirectStub.lastCall.args[0],
@@ -711,15 +703,15 @@
       );
     });
 
-    test('_handleAgreementsRoute', () => {
-      element._handleAgreementsRoute();
+    test('handleAgreementsRoute', () => {
+      router.handleAgreementsRoute();
       assert.isTrue(redirectStub.calledOnce);
       assert.equal(redirectStub.lastCall.args[0], '/settings/#Agreements');
     });
 
-    test('_handleNewAgreementsRoute', () => {
+    test('handleNewAgreementsRoute', () => {
       const params = createPageContext();
-      element._handleNewAgreementsRoute(params);
+      router.handleNewAgreementsRoute(params);
       assert.isTrue(setParamsStub.calledOnce);
       assert.equal(
         setParamsStub.lastCall.args[0].view,
@@ -727,38 +719,38 @@
       );
     });
 
-    test('_handleSettingsLegacyRoute', () => {
+    test('handleSettingsLegacyRoute', () => {
       const data = {...createPageContext(), params: {0: 'my-token'}};
-      assertDataToParams(data, '_handleSettingsLegacyRoute', {
+      assertDataToParams(data, 'handleSettingsLegacyRoute', {
         view: GerritNav.View.SETTINGS,
         emailToken: 'my-token',
       });
     });
 
-    test('_handleSettingsLegacyRoute with +', () => {
+    test('handleSettingsLegacyRoute with +', () => {
       const data = {...createPageContext(), params: {0: 'my-token test'}};
-      assertDataToParams(data, '_handleSettingsLegacyRoute', {
+      assertDataToParams(data, 'handleSettingsLegacyRoute', {
         view: GerritNav.View.SETTINGS,
         emailToken: 'my-token+test',
       });
     });
 
-    test('_handleSettingsRoute', () => {
+    test('handleSettingsRoute', () => {
       const data = createPageContext();
-      assertDataToParams(data, '_handleSettingsRoute', {
+      assertDataToParams(data, 'handleSettingsRoute', {
         view: GerritNav.View.SETTINGS,
       });
     });
 
-    test('_handleDefaultRoute on first load', () => {
+    test('handleDefaultRoute on first load', () => {
       const spy = sinon.spy();
       addListenerForTest(document, 'page-error', spy);
-      element._handleDefaultRoute();
+      router.handleDefaultRoute();
       assert.isTrue(spy.calledOnce);
       assert.equal(spy.lastCall.args[0].detail.response.status, 404);
     });
 
-    test('_handleDefaultRoute after internal navigation', () => {
+    test('handleDefaultRoute after internal navigation', () => {
       let onExit: Function | null = null;
       const onRegisteringExit = (
         _match: string | RegExp,
@@ -770,38 +762,38 @@
       sinon.stub(GerritNav, 'setup');
       sinon.stub(page, 'start');
       sinon.stub(page, 'base');
-      element._startRouter();
+      router.startRouter();
 
-      element._handleDefaultRoute();
+      router.handleDefaultRoute();
 
       onExit!('', () => {}); // we left page;
 
-      element._handleDefaultRoute();
+      router.handleDefaultRoute();
       assert.isTrue(handlePassThroughRoute.calledOnce);
     });
 
-    test('_handleImproperlyEncodedPlusRoute', () => {
+    test('handleImproperlyEncodedPlusRoute', () => {
       const params = {
         ...createPageContext(),
         canonicalPath: '/c/test/%20/42',
         params: {0: 'test', 1: '42'},
       };
       // Regression test for Issue 7100.
-      element._handleImproperlyEncodedPlusRoute(params);
+      router.handleImproperlyEncodedPlusRoute(params);
       assert.isTrue(redirectStub.calledOnce);
       assert.equal(redirectStub.lastCall.args[0], '/c/test/+/42');
 
-      sinon.stub(element, '_getHashFromCanonicalPath').returns('foo');
-      element._handleImproperlyEncodedPlusRoute(params);
+      sinon.stub(router, 'getHashFromCanonicalPath').returns('foo');
+      router.handleImproperlyEncodedPlusRoute(params);
       assert.equal(redirectStub.lastCall.args[0], '/c/test/+/42#foo');
     });
 
-    test('_handleQueryRoute', () => {
+    test('handleQueryRoute', () => {
       const data: PageContextWithQueryMap = {
         ...createPageContext(),
         params: {0: 'project:foo/bar/baz'},
       };
-      assertDataToParams(data, '_handleQueryRoute', {
+      assertDataToParams(data, 'handleQueryRoute', {
         view: GerritNav.View.SEARCH,
         query: 'project:foo/bar/baz',
         offset: undefined,
@@ -809,35 +801,35 @@
 
       data.params[1] = '123';
       data.params[2] = '123';
-      assertDataToParams(data, '_handleQueryRoute', {
+      assertDataToParams(data, 'handleQueryRoute', {
         view: GerritNav.View.SEARCH,
         query: 'project:foo/bar/baz',
         offset: '123',
       });
     });
 
-    test('_handleQueryLegacySuffixRoute', () => {
+    test('handleQueryLegacySuffixRoute', () => {
       const params = {...createPageContext(), path: '/q/foo+bar,n,z'};
-      element._handleQueryLegacySuffixRoute(params);
+      router.handleQueryLegacySuffixRoute(params);
       assert.isTrue(redirectStub.calledOnce);
       assert.equal(redirectStub.lastCall.args[0], '/q/foo+bar');
     });
 
-    test('_handleChangeIdQueryRoute', () => {
+    test('handleChangeIdQueryRoute', () => {
       const data = {
         ...createPageContext(),
         params: {0: 'I0123456789abcdef0123456789abcdef01234567'},
       };
-      assertDataToParams(data, '_handleChangeIdQueryRoute', {
+      assertDataToParams(data, 'handleChangeIdQueryRoute', {
         view: GerritNav.View.SEARCH,
         query: 'I0123456789abcdef0123456789abcdef01234567',
       });
     });
 
-    suite('_handleRegisterRoute', () => {
+    suite('handleRegisterRoute', () => {
       test('happy path', () => {
         const ctx = {...createPageContext(), params: {0: '/foo/bar'}};
-        element._handleRegisterRoute(ctx);
+        router.handleRegisterRoute(ctx);
         assert.isTrue(redirectStub.calledWithExactly('/foo/bar'));
         assert.isTrue(setParamsStub.calledOnce);
         assert.isTrue(setParamsStub.lastCall.args[0].justRegistered);
@@ -845,7 +837,7 @@
 
       test('no param', () => {
         const ctx = createPageContext();
-        element._handleRegisterRoute(ctx);
+        router.handleRegisterRoute(ctx);
         assert.isTrue(redirectStub.calledWithExactly('/'));
         assert.isTrue(setParamsStub.calledOnce);
         assert.isTrue(setParamsStub.lastCall.args[0].justRegistered);
@@ -853,18 +845,18 @@
 
       test('prevent redirect', () => {
         const ctx = {...createPageContext(), params: {0: '/register'}};
-        element._handleRegisterRoute(ctx);
+        router.handleRegisterRoute(ctx);
         assert.isTrue(redirectStub.calledWithExactly('/'));
         assert.isTrue(setParamsStub.calledOnce);
         assert.isTrue(setParamsStub.lastCall.args[0].justRegistered);
       });
     });
 
-    suite('_handleRootRoute', () => {
+    suite('handleRootRoute', () => {
       test('closes for closeAfterLogin', () => {
         const data = {...createPageContext(), querystring: 'closeAfterLogin'};
         const closeStub = sinon.stub(window, 'close');
-        const result = element._handleRootRoute(data);
+        const result = router.handleRootRoute(data);
         assert.isNotOk(result);
         assert.isTrue(closeStub.called);
         assert.isFalse(redirectStub.called);
@@ -872,7 +864,7 @@
 
       test('redirects to dashboard if logged in', () => {
         const data = {...createPageContext(), canonicalPath: '/', path: '/'};
-        const result = element._handleRootRoute(data);
+        const result = router.handleRootRoute(data);
         assert.isOk(result);
         return result!.then(() => {
           assert.isTrue(redirectStub.calledWithExactly('/dashboard/self'));
@@ -882,7 +874,7 @@
       test('redirects to open changes if not logged in', () => {
         stubRestApi('getLoggedIn').returns(Promise.resolve(false));
         const data = {...createPageContext(), canonicalPath: '/', path: '/'};
-        const result = element._handleRootRoute(data);
+        const result = router.handleRootRoute(data);
         assert.isOk(result);
         return result!.then(() => {
           assert.isTrue(
@@ -898,7 +890,7 @@
             canonicalPath: '/#/foo/bar/baz',
             hash: '/foo/bar/baz',
           };
-          const result = element._handleRootRoute(data);
+          const result = router.handleRootRoute(data);
           assert.isNotOk(result);
           assert.isTrue(redirectStub.called);
           assert.isTrue(redirectStub.calledWithExactly('/foo/bar/baz'));
@@ -910,7 +902,7 @@
             canonicalPath: '/#foo/bar/baz',
             hash: 'foo/bar/baz',
           };
-          const result = element._handleRootRoute(data);
+          const result = router.handleRootRoute(data);
           assert.isNotOk(result);
           assert.isTrue(redirectStub.called);
           assert.isTrue(redirectStub.calledWithExactly('/foo/bar/baz'));
@@ -922,7 +914,7 @@
             canonicalPath: '/#/foo/bar/+/123/4',
             hash: '/foo/bar/ /123/4',
           };
-          const result = element._handleRootRoute(data);
+          const result = router.handleRootRoute(data);
           assert.isNotOk(result);
           assert.isTrue(redirectStub.called);
           assert.isTrue(redirectStub.calledWithExactly('/foo/bar/+/123/4'));
@@ -935,7 +927,7 @@
             hash: '/foo/bar',
           };
           stubBaseUrl('/baz');
-          const result = element._handleRootRoute(data);
+          const result = router.handleRootRoute(data);
           assert.isNotOk(result);
           assert.isTrue(redirectStub.called);
           assert.isTrue(redirectStub.calledWithExactly('/baz/foo/bar'));
@@ -947,7 +939,7 @@
             canonicalPath: '/#/VE/foo/bar',
             hash: '/VE/foo/bar',
           };
-          const result = element._handleRootRoute(data);
+          const result = router.handleRootRoute(data);
           assert.isNotOk(result);
           assert.isTrue(redirectStub.called);
           assert.isTrue(redirectStub.calledWithExactly('/settings/VE/foo/bar'));
@@ -959,7 +951,7 @@
             canonicalPath: '/#/foo/bar#baz',
             hash: '/foo/bar',
           };
-          const result = element._handleRootRoute(data);
+          const result = router.handleRootRoute(data);
           assert.isNotOk(result);
           assert.isTrue(redirectStub.called);
           assert.isTrue(redirectStub.calledWithExactly('/foo/bar#baz'));
@@ -967,11 +959,11 @@
       });
     });
 
-    suite('_handleDashboardRoute', () => {
+    suite('handleDashboardRoute', () => {
       let redirectToLoginStub: sinon.SinonStub;
 
       setup(() => {
-        redirectToLoginStub = sinon.stub(element, '_redirectToLogin');
+        redirectToLoginStub = sinon.stub(router, 'redirectToLogin');
       });
 
       test('own dashboard but signed out redirects to login', () => {
@@ -981,7 +973,7 @@
           canonicalPath: '/dashboard/',
           params: {0: 'seLF'},
         };
-        return element._handleDashboardRoute(data).then(() => {
+        return router.handleDashboardRoute(data).then(() => {
           assert.isTrue(redirectToLoginStub.calledOnce);
           assert.isFalse(redirectStub.called);
           assert.isFalse(setParamsStub.called);
@@ -995,7 +987,7 @@
           canonicalPath: '/dashboard/',
           params: {0: 'foo'},
         };
-        return element._handleDashboardRoute(data).then(() => {
+        return router.handleDashboardRoute(data).then(() => {
           assert.isFalse(redirectToLoginStub.called);
           assert.isFalse(setParamsStub.called);
           assert.isTrue(redirectStub.calledOnce);
@@ -1009,7 +1001,7 @@
           canonicalPath: '/dashboard/',
           params: {0: 'foo'},
         };
-        return element._handleDashboardRoute(data).then(() => {
+        return router.handleDashboardRoute(data).then(() => {
           assert.isFalse(redirectToLoginStub.called);
           assert.isFalse(redirectStub.called);
           assert.isTrue(setParamsStub.calledOnce);
@@ -1021,11 +1013,11 @@
       });
     });
 
-    suite('_handleCustomDashboardRoute', () => {
+    suite('handleCustomDashboardRoute', () => {
       let redirectToLoginStub: sinon.SinonStub;
 
       setup(() => {
-        redirectToLoginStub = sinon.stub(element, '_redirectToLogin');
+        redirectToLoginStub = sinon.stub(router, 'redirectToLogin');
       });
 
       test('no user specified', () => {
@@ -1034,7 +1026,7 @@
           canonicalPath: '/dashboard/',
           params: {0: ''},
         };
-        return element._handleCustomDashboardRoute(data, '').then(() => {
+        return router.handleCustomDashboardRoute(data, '').then(() => {
           assert.isFalse(setParamsStub.called);
           assert.isTrue(redirectStub.called);
           assert.equal(redirectStub.lastCall.args[0], '/dashboard/self');
@@ -1047,8 +1039,8 @@
           canonicalPath: '/dashboard/',
           params: {0: ''},
         };
-        return element
-          ._handleCustomDashboardRoute(data, '?a=b&c&d=e')
+        return router
+          .handleCustomDashboardRoute(data, '?a=b&c&d=e')
           .then(() => {
             assert.isFalse(redirectStub.called);
             assert.isTrue(setParamsStub.calledOnce);
@@ -1070,8 +1062,8 @@
           canonicalPath: '/dashboard/',
           params: {0: ''},
         };
-        return element
-          ._handleCustomDashboardRoute(data, '?a=b&c&d=&=e&title=t')
+        return router
+          .handleCustomDashboardRoute(data, '?a=b&c&d=&=e&title=t')
           .then(() => {
             assert.isFalse(redirectToLoginStub.called);
             assert.isFalse(redirectStub.called);
@@ -1091,8 +1083,8 @@
           canonicalPath: '/dashboard/',
           params: {0: ''},
         };
-        return element
-          ._handleCustomDashboardRoute(data, '?a=b&c&d=&=e&foreach=is:open')
+        return router
+          .handleCustomDashboardRoute(data, '?a=b&c&d=&=e&foreach=is:open')
           .then(() => {
             assert.isFalse(redirectToLoginStub.called);
             assert.isFalse(redirectStub.called);
@@ -1108,34 +1100,34 @@
     });
 
     suite('group routes', () => {
-      test('_handleGroupInfoRoute', () => {
+      test('handleGroupInfoRoute', () => {
         const data = {...createPageContext(), params: {0: '1234'}};
-        element._handleGroupInfoRoute(data);
+        router.handleGroupInfoRoute(data);
         assert.isTrue(redirectStub.calledOnce);
         assert.equal(redirectStub.lastCall.args[0], '/admin/groups/1234');
       });
 
-      test('_handleGroupAuditLogRoute', () => {
+      test('handleGroupAuditLogRoute', () => {
         const data = {...createPageContext(), params: {0: '1234'}};
-        assertDataToParams(data, '_handleGroupAuditLogRoute', {
+        assertDataToParams(data, 'handleGroupAuditLogRoute', {
           view: GerritView.GROUP,
           detail: GroupDetailView.LOG,
           groupId: '1234' as GroupId,
         });
       });
 
-      test('_handleGroupMembersRoute', () => {
+      test('handleGroupMembersRoute', () => {
         const data = {...createPageContext(), params: {0: '1234'}};
-        assertDataToParams(data, '_handleGroupMembersRoute', {
+        assertDataToParams(data, 'handleGroupMembersRoute', {
           view: GerritView.GROUP,
           detail: GroupDetailView.MEMBERS,
           groupId: '1234' as GroupId,
         });
       });
 
-      test('_handleGroupListOffsetRoute', () => {
+      test('handleGroupListOffsetRoute', () => {
         const data = createPageContext();
-        assertDataToParams(data, '_handleGroupListOffsetRoute', {
+        assertDataToParams(data, 'handleGroupListOffsetRoute', {
           view: GerritView.ADMIN,
           adminView: 'gr-admin-group-list',
           offset: 0,
@@ -1144,7 +1136,7 @@
         });
 
         data.params[1] = '42';
-        assertDataToParams(data, '_handleGroupListOffsetRoute', {
+        assertDataToParams(data, 'handleGroupListOffsetRoute', {
           view: GerritView.ADMIN,
           adminView: 'gr-admin-group-list',
           offset: '42',
@@ -1153,7 +1145,7 @@
         });
 
         data.hash = 'create';
-        assertDataToParams(data, '_handleGroupListOffsetRoute', {
+        assertDataToParams(data, 'handleGroupListOffsetRoute', {
           view: GerritNav.View.ADMIN,
           adminView: 'gr-admin-group-list',
           offset: '42',
@@ -1162,12 +1154,12 @@
         });
       });
 
-      test('_handleGroupListFilterOffsetRoute', () => {
+      test('handleGroupListFilterOffsetRoute', () => {
         const data = {
           ...createPageContext(),
           params: {filter: 'foo', offset: '42'},
         };
-        assertDataToParams(data, '_handleGroupListFilterOffsetRoute', {
+        assertDataToParams(data, 'handleGroupListFilterOffsetRoute', {
           view: GerritView.ADMIN,
           adminView: 'gr-admin-group-list',
           offset: '42',
@@ -1175,18 +1167,18 @@
         });
       });
 
-      test('_handleGroupListFilterRoute', () => {
+      test('handleGroupListFilterRoute', () => {
         const data = {...createPageContext(), params: {filter: 'foo'}};
-        assertDataToParams(data, '_handleGroupListFilterRoute', {
+        assertDataToParams(data, 'handleGroupListFilterRoute', {
           view: GerritView.ADMIN,
           adminView: 'gr-admin-group-list',
           filter: 'foo',
         });
       });
 
-      test('_handleGroupRoute', () => {
+      test('handleGroupRoute', () => {
         const data = {...createPageContext(), params: {0: '4321'}};
-        assertDataToParams(data, '_handleGroupRoute', {
+        assertDataToParams(data, 'handleGroupRoute', {
           view: GerritView.GROUP,
           groupId: '4321' as GroupId,
         });
@@ -1194,23 +1186,23 @@
     });
 
     suite('repo routes', () => {
-      test('_handleProjectsOldRoute', () => {
+      test('handleProjectsOldRoute', () => {
         const data = {...createPageContext(), params: {}};
-        element._handleProjectsOldRoute(data);
+        router.handleProjectsOldRoute(data);
         assert.isTrue(redirectStub.calledOnce);
         assert.equal(redirectStub.lastCall.args[0], '/admin/repos/');
       });
 
-      test('_handleProjectsOldRoute test', () => {
+      test('handleProjectsOldRoute test', () => {
         const data = {...createPageContext(), params: {1: 'test'}};
-        element._handleProjectsOldRoute(data);
+        router.handleProjectsOldRoute(data);
         assert.isTrue(redirectStub.calledOnce);
         assert.equal(redirectStub.lastCall.args[0], '/admin/repos/test');
       });
 
-      test('_handleProjectsOldRoute test,branches', () => {
+      test('handleProjectsOldRoute test,branches', () => {
         const data = {...createPageContext(), params: {1: 'test,branches'}};
-        element._handleProjectsOldRoute(data);
+        router.handleProjectsOldRoute(data);
         assert.isTrue(redirectStub.calledOnce);
         assert.equal(
           redirectStub.lastCall.args[0],
@@ -1218,9 +1210,9 @@
         );
       });
 
-      test('_handleRepoRoute', () => {
+      test('handleRepoRoute', () => {
         const data = {...createPageContext(), path: '/admin/repos/test'};
-        element._handleRepoRoute(data);
+        router.handleRepoRoute(data);
         assert.isTrue(redirectStub.calledOnce);
         assert.equal(
           redirectStub.lastCall.args[0],
@@ -1228,27 +1220,27 @@
         );
       });
 
-      test('_handleRepoGeneralRoute', () => {
+      test('handleRepoGeneralRoute', () => {
         const data = {...createPageContext(), params: {0: '4321'}};
-        assertDataToParams(data, '_handleRepoGeneralRoute', {
+        assertDataToParams(data, 'handleRepoGeneralRoute', {
           view: GerritView.REPO,
           detail: GerritNav.RepoDetailView.GENERAL,
           repo: '4321' as RepoName,
         });
       });
 
-      test('_handleRepoCommandsRoute', () => {
+      test('handleRepoCommandsRoute', () => {
         const data = {...createPageContext(), params: {0: '4321'}};
-        assertDataToParams(data, '_handleRepoCommandsRoute', {
+        assertDataToParams(data, 'handleRepoCommandsRoute', {
           view: GerritView.REPO,
           detail: GerritNav.RepoDetailView.COMMANDS,
           repo: '4321' as RepoName,
         });
       });
 
-      test('_handleRepoAccessRoute', () => {
+      test('handleRepoAccessRoute', () => {
         const data = {...createPageContext(), params: {0: '4321'}};
-        assertDataToParams(data, '_handleRepoAccessRoute', {
+        assertDataToParams(data, 'handleRepoAccessRoute', {
           view: GerritView.REPO,
           detail: GerritNav.RepoDetailView.ACCESS,
           repo: '4321' as RepoName,
@@ -1256,12 +1248,12 @@
       });
 
       suite('branch list routes', () => {
-        test('_handleBranchListOffsetRoute', () => {
+        test('handleBranchListOffsetRoute', () => {
           const data: PageContextWithQueryMap = {
             ...createPageContext(),
             params: {0: '4321'},
           };
-          assertDataToParams(data, '_handleBranchListOffsetRoute', {
+          assertDataToParams(data, 'handleBranchListOffsetRoute', {
             view: GerritView.REPO,
             detail: GerritNav.RepoDetailView.BRANCHES,
             repo: '4321' as RepoName,
@@ -1270,7 +1262,7 @@
           });
 
           data.params[2] = '42';
-          assertDataToParams(data, '_handleBranchListOffsetRoute', {
+          assertDataToParams(data, 'handleBranchListOffsetRoute', {
             view: GerritView.REPO,
             detail: GerritNav.RepoDetailView.BRANCHES,
             repo: '4321' as RepoName,
@@ -1279,12 +1271,12 @@
           });
         });
 
-        test('_handleBranchListFilterOffsetRoute', () => {
+        test('handleBranchListFilterOffsetRoute', () => {
           const data = {
             ...createPageContext(),
             params: {repo: '4321', filter: 'foo', offset: '42'},
           };
-          assertDataToParams(data, '_handleBranchListFilterOffsetRoute', {
+          assertDataToParams(data, 'handleBranchListFilterOffsetRoute', {
             view: GerritView.REPO,
             detail: GerritNav.RepoDetailView.BRANCHES,
             repo: '4321' as RepoName,
@@ -1293,12 +1285,12 @@
           });
         });
 
-        test('_handleBranchListFilterRoute', () => {
+        test('handleBranchListFilterRoute', () => {
           const data = {
             ...createPageContext(),
             params: {repo: '4321', filter: 'foo'},
           };
-          assertDataToParams(data, '_handleBranchListFilterRoute', {
+          assertDataToParams(data, 'handleBranchListFilterRoute', {
             view: GerritView.REPO,
             detail: GerritNav.RepoDetailView.BRANCHES,
             repo: '4321' as RepoName,
@@ -1308,9 +1300,9 @@
       });
 
       suite('tag list routes', () => {
-        test('_handleTagListOffsetRoute', () => {
+        test('handleTagListOffsetRoute', () => {
           const data = {...createPageContext(), params: {0: '4321'}};
-          assertDataToParams(data, '_handleTagListOffsetRoute', {
+          assertDataToParams(data, 'handleTagListOffsetRoute', {
             view: GerritView.REPO,
             detail: GerritNav.RepoDetailView.TAGS,
             repo: '4321' as RepoName,
@@ -1319,12 +1311,12 @@
           });
         });
 
-        test('_handleTagListFilterOffsetRoute', () => {
+        test('handleTagListFilterOffsetRoute', () => {
           const data = {
             ...createPageContext(),
             params: {repo: '4321', filter: 'foo', offset: '42'},
           };
-          assertDataToParams(data, '_handleTagListFilterOffsetRoute', {
+          assertDataToParams(data, 'handleTagListFilterOffsetRoute', {
             view: GerritView.REPO,
             detail: GerritNav.RepoDetailView.TAGS,
             repo: '4321' as RepoName,
@@ -1333,12 +1325,12 @@
           });
         });
 
-        test('_handleTagListFilterRoute', () => {
+        test('handleTagListFilterRoute', () => {
           const data: PageContextWithQueryMap = {
             ...createPageContext(),
             params: {repo: '4321'},
           };
-          assertDataToParams(data, '_handleTagListFilterRoute', {
+          assertDataToParams(data, 'handleTagListFilterRoute', {
             view: GerritView.REPO,
             detail: GerritNav.RepoDetailView.TAGS,
             repo: '4321' as RepoName,
@@ -1346,7 +1338,7 @@
           });
 
           data.params.filter = 'foo';
-          assertDataToParams(data, '_handleTagListFilterRoute', {
+          assertDataToParams(data, 'handleTagListFilterRoute', {
             view: GerritView.REPO,
             detail: GerritNav.RepoDetailView.TAGS,
             repo: '4321' as RepoName,
@@ -1356,9 +1348,9 @@
       });
 
       suite('repo list routes', () => {
-        test('_handleRepoListOffsetRoute', () => {
+        test('handleRepoListOffsetRoute', () => {
           const data = createPageContext();
-          assertDataToParams(data, '_handleRepoListOffsetRoute', {
+          assertDataToParams(data, 'handleRepoListOffsetRoute', {
             view: GerritView.ADMIN,
             adminView: 'gr-repo-list',
             offset: 0,
@@ -1367,7 +1359,7 @@
           });
 
           data.params[1] = '42';
-          assertDataToParams(data, '_handleRepoListOffsetRoute', {
+          assertDataToParams(data, 'handleRepoListOffsetRoute', {
             view: GerritView.ADMIN,
             adminView: 'gr-repo-list',
             offset: '42',
@@ -1376,7 +1368,7 @@
           });
 
           data.hash = 'create';
-          assertDataToParams(data, '_handleRepoListOffsetRoute', {
+          assertDataToParams(data, 'handleRepoListOffsetRoute', {
             view: GerritView.ADMIN,
             adminView: 'gr-repo-list',
             offset: '42',
@@ -1385,12 +1377,12 @@
           });
         });
 
-        test('_handleRepoListFilterOffsetRoute', () => {
+        test('handleRepoListFilterOffsetRoute', () => {
           const data = {
             ...createPageContext(),
             params: {filter: 'foo', offset: '42'},
           };
-          assertDataToParams(data, '_handleRepoListFilterOffsetRoute', {
+          assertDataToParams(data, 'handleRepoListFilterOffsetRoute', {
             view: GerritView.ADMIN,
             adminView: 'gr-repo-list',
             offset: '42',
@@ -1398,16 +1390,16 @@
           });
         });
 
-        test('_handleRepoListFilterRoute', () => {
+        test('handleRepoListFilterRoute', () => {
           const data = createPageContext();
-          assertDataToParams(data, '_handleRepoListFilterRoute', {
+          assertDataToParams(data, 'handleRepoListFilterRoute', {
             view: GerritView.ADMIN,
             adminView: 'gr-repo-list',
             filter: null,
           });
 
           data.params.filter = 'foo';
-          assertDataToParams(data, '_handleRepoListFilterRoute', {
+          assertDataToParams(data, 'handleRepoListFilterRoute', {
             view: GerritView.ADMIN,
             adminView: 'gr-repo-list',
             filter: 'foo',
@@ -1417,9 +1409,9 @@
     });
 
     suite('plugin routes', () => {
-      test('_handlePluginListOffsetRoute', () => {
+      test('handlePluginListOffsetRoute', () => {
         const data = createPageContext();
-        assertDataToParams(data, '_handlePluginListOffsetRoute', {
+        assertDataToParams(data, 'handlePluginListOffsetRoute', {
           view: GerritView.ADMIN,
           adminView: 'gr-plugin-list',
           offset: 0,
@@ -1427,7 +1419,7 @@
         });
 
         data.params[1] = '42';
-        assertDataToParams(data, '_handlePluginListOffsetRoute', {
+        assertDataToParams(data, 'handlePluginListOffsetRoute', {
           view: GerritView.ADMIN,
           adminView: 'gr-plugin-list',
           offset: '42',
@@ -1435,12 +1427,12 @@
         });
       });
 
-      test('_handlePluginListFilterOffsetRoute', () => {
+      test('handlePluginListFilterOffsetRoute', () => {
         const data = {
           ...createPageContext(),
           params: {filter: 'foo', offset: '42'},
         };
-        assertDataToParams(data, '_handlePluginListFilterOffsetRoute', {
+        assertDataToParams(data, 'handlePluginListFilterOffsetRoute', {
           view: GerritView.ADMIN,
           adminView: 'gr-plugin-list',
           offset: '42',
@@ -1448,25 +1440,25 @@
         });
       });
 
-      test('_handlePluginListFilterRoute', () => {
+      test('handlePluginListFilterRoute', () => {
         const data = createPageContext();
-        assertDataToParams(data, '_handlePluginListFilterRoute', {
+        assertDataToParams(data, 'handlePluginListFilterRoute', {
           view: GerritView.ADMIN,
           adminView: 'gr-plugin-list',
           filter: null,
         });
 
         data.params.filter = 'foo';
-        assertDataToParams(data, '_handlePluginListFilterRoute', {
+        assertDataToParams(data, 'handlePluginListFilterRoute', {
           view: GerritView.ADMIN,
           adminView: 'gr-plugin-list',
           filter: 'foo',
         });
       });
 
-      test('_handlePluginListRoute', () => {
+      test('handlePluginListRoute', () => {
         const data = createPageContext();
-        assertDataToParams(data, '_handlePluginListRoute', {
+        assertDataToParams(data, 'handlePluginListRoute', {
           view: GerritView.ADMIN,
           adminView: 'gr-plugin-list',
         });
@@ -1474,14 +1466,14 @@
     });
 
     suite('change/diff routes', () => {
-      test('_handleChangeNumberLegacyRoute', () => {
+      test('handleChangeNumberLegacyRoute', () => {
         const data = {...createPageContext(), params: {0: '12345'}};
-        element._handleChangeNumberLegacyRoute(data);
+        router.handleChangeNumberLegacyRoute(data);
         assert.isTrue(redirectStub.calledOnce);
         assert.isTrue(redirectStub.calledWithExactly('/c/12345'));
       });
 
-      test('_handleChangeLegacyRoute', async () => {
+      test('handleChangeLegacyRoute', async () => {
         stubRestApi('getFromProjectLookup').returns(
           Promise.resolve('project' as RepoName)
         );
@@ -1489,32 +1481,32 @@
           ...createPageContext(),
           params: {0: '1234', 1: 'comment/6789'},
         };
-        element._handleChangeLegacyRoute(ctx);
+        router.handleChangeLegacyRoute(ctx);
         await flush();
         assert.isTrue(
           redirectStub.calledWithExactly('/c/project/+/1234' + '/comment/6789')
         );
       });
 
-      test('_handleLegacyLinenum w/ @321', () => {
+      test('handleLegacyLinenum w/ @321', () => {
         const ctx = {...createPageContext(), path: '/c/1234/3..8/foo/bar@321'};
-        element._handleLegacyLinenum(ctx);
+        router.handleLegacyLinenum(ctx);
         assert.isTrue(redirectStub.calledOnce);
         assert.isTrue(
           redirectStub.calledWithExactly('/c/1234/3..8/foo/bar#321')
         );
       });
 
-      test('_handleLegacyLinenum w/ @b123', () => {
+      test('handleLegacyLinenum w/ @b123', () => {
         const ctx = {...createPageContext(), path: '/c/1234/3..8/foo/bar@b123'};
-        element._handleLegacyLinenum(ctx);
+        router.handleLegacyLinenum(ctx);
         assert.isTrue(redirectStub.calledOnce);
         assert.isTrue(
           redirectStub.calledWithExactly('/c/1234/3..8/foo/bar#b123')
         );
       });
 
-      suite('_handleChangeRoute', () => {
+      suite('handleChangeRoute', () => {
         let normalizeRangeStub: sinon.SinonStub;
 
         function makeParams(
@@ -1536,18 +1528,15 @@
         }
 
         setup(() => {
-          normalizeRangeStub = sinon.stub(
-            element,
-            '_normalizePatchRangeParams'
-          );
+          normalizeRangeStub = sinon.stub(router, 'normalizePatchRangeParams');
           stubRestApi('setInProjectLookup');
         });
 
         test('needs redirect', () => {
           normalizeRangeStub.returns(true);
-          sinon.stub(element, '_generateUrl').returns('foo');
+          sinon.stub(router, 'generateUrl').returns('foo');
           const ctx = makeParams('', '');
-          element._handleChangeRoute(ctx);
+          router.handleChangeRoute(ctx);
           assert.isTrue(normalizeRangeStub.called);
           assert.isFalse(setParamsStub.called);
           assert.isTrue(redirectStub.calledOnce);
@@ -1556,9 +1545,9 @@
 
         test('change view', () => {
           normalizeRangeStub.returns(false);
-          sinon.stub(element, '_generateUrl').returns('foo');
+          sinon.stub(router, 'generateUrl').returns('foo');
           const ctx = makeParams('', '');
-          assertDataToParams(ctx, '_handleChangeRoute', {
+          assertDataToParams(ctx, 'handleChangeRoute', {
             view: GerritView.CHANGE,
             project: 'foo/bar' as RepoName,
             changeNum: 1234 as NumericChangeId,
@@ -1571,13 +1560,13 @@
 
         test('params', () => {
           normalizeRangeStub.returns(false);
-          sinon.stub(element, '_generateUrl').returns('foo');
+          sinon.stub(router, 'generateUrl').returns('foo');
           const ctx = makeParams('', '');
           ctx.queryMap.set('tab', 'checks');
           ctx.queryMap.set('filter', 'fff');
           ctx.queryMap.set('select', 'sss');
           ctx.queryMap.set('attempt', '1');
-          assertDataToParams(ctx, '_handleChangeRoute', {
+          assertDataToParams(ctx, 'handleChangeRoute', {
             view: GerritView.CHANGE,
             project: 'foo/bar' as RepoName,
             changeNum: 1234 as NumericChangeId,
@@ -1591,7 +1580,7 @@
         });
       });
 
-      suite('_handleDiffRoute', () => {
+      suite('handleDiffRoute', () => {
         let normalizeRangeStub: sinon.SinonStub;
 
         function makeParams(
@@ -1616,18 +1605,15 @@
         }
 
         setup(() => {
-          normalizeRangeStub = sinon.stub(
-            element,
-            '_normalizePatchRangeParams'
-          );
+          normalizeRangeStub = sinon.stub(router, 'normalizePatchRangeParams');
           stubRestApi('setInProjectLookup');
         });
 
         test('needs redirect', () => {
           normalizeRangeStub.returns(true);
-          sinon.stub(element, '_generateUrl').returns('foo');
+          sinon.stub(router, 'generateUrl').returns('foo');
           const ctx = makeParams('', '');
-          element._handleDiffRoute(ctx);
+          router.handleDiffRoute(ctx);
           assert.isTrue(normalizeRangeStub.called);
           assert.isFalse(setParamsStub.called);
           assert.isTrue(redirectStub.calledOnce);
@@ -1636,9 +1622,9 @@
 
         test('diff view', () => {
           normalizeRangeStub.returns(false);
-          sinon.stub(element, '_generateUrl').returns('foo');
+          sinon.stub(router, 'generateUrl').returns('foo');
           const ctx = makeParams('foo/bar/baz', 'b44');
-          assertDataToParams(ctx, '_handleDiffRoute', {
+          assertDataToParams(ctx, 'handleDiffRoute', {
             view: GerritView.DIFF,
             project: 'foo/bar' as RepoName,
             changeNum: 1234 as NumericChangeId,
@@ -1662,7 +1648,7 @@
           ]);
           assertDataToParams(
             {params: groups!.slice(1)} as any,
-            '_handleCommentRoute',
+            'handleCommentRoute',
             {
               project: 'gerrit' as RepoName,
               changeNum: 264833 as NumericChangeId,
@@ -1683,7 +1669,7 @@
           ]);
           assertDataToParams(
             {params: groups!.slice(1)} as any,
-            '_handleCommentsRoute',
+            'handleCommentsRoute',
             {
               project: 'gerrit' as RepoName,
               changeNum: 264833 as NumericChangeId,
@@ -1694,10 +1680,10 @@
         });
       });
 
-      test('_handleDiffEditRoute', () => {
+      test('handleDiffEditRoute', () => {
         const normalizeRangeSpy = sinon.spy(
-          element,
-          '_normalizePatchRangeParams'
+          router,
+          'normalizePatchRangeParams'
         );
         stubRestApi('setInProjectLookup');
         const ctx = {
@@ -1719,7 +1705,7 @@
           lineNum: '',
         };
 
-        element._handleDiffEditRoute(ctx);
+        router.handleDiffEditRoute(ctx);
         assert.isFalse(redirectStub.called);
         assert.isTrue(normalizeRangeSpy.calledOnce);
         assert.deepEqual(normalizeRangeSpy.lastCall.args[0], appParams);
@@ -1727,10 +1713,10 @@
         assert.deepEqual(setParamsStub.lastCall.args[0], appParams);
       });
 
-      test('_handleDiffEditRoute with lineNum', () => {
+      test('handleDiffEditRoute with lineNum', () => {
         const normalizeRangeSpy = sinon.spy(
-          element,
-          '_normalizePatchRangeParams'
+          router,
+          'normalizePatchRangeParams'
         );
         stubRestApi('setInProjectLookup');
         const ctx = {
@@ -1752,7 +1738,7 @@
           lineNum: '4',
         };
 
-        element._handleDiffEditRoute(ctx);
+        router.handleDiffEditRoute(ctx);
         assert.isFalse(redirectStub.called);
         assert.isTrue(normalizeRangeSpy.calledOnce);
         assert.deepEqual(normalizeRangeSpy.lastCall.args[0], appParams);
@@ -1760,10 +1746,10 @@
         assert.deepEqual(setParamsStub.lastCall.args[0], appParams);
       });
 
-      test('_handleChangeEditRoute', () => {
+      test('handleChangeEditRoute', () => {
         const normalizeRangeSpy = sinon.spy(
-          element,
-          '_normalizePatchRangeParams'
+          router,
+          'normalizePatchRangeParams'
         );
         stubRestApi('setInProjectLookup');
         const ctx = {
@@ -1784,7 +1770,7 @@
           tab: '',
         };
 
-        element._handleChangeEditRoute(ctx);
+        router.handleChangeEditRoute(ctx);
         assert.isFalse(redirectStub.called);
         assert.isTrue(normalizeRangeSpy.calledOnce);
         assert.deepEqual(normalizeRangeSpy.lastCall.args[0], appParams);
@@ -1793,9 +1779,9 @@
       });
     });
 
-    test('_handlePluginScreen', () => {
+    test('handlePluginScreen', () => {
       const ctx = {...createPageContext(), params: {0: 'foo', 1: 'bar'}};
-      assertDataToParams(ctx, '_handlePluginScreen', {
+      assertDataToParams(ctx, 'handlePluginScreen', {
         view: GerritNav.View.PLUGIN_SCREEN,
         plugin: 'foo',
         screen: 'bar',
@@ -1804,30 +1790,30 @@
     });
   });
 
-  suite('_parseQueryString', () => {
+  suite('parseQueryString', () => {
     test('empty queries', () => {
-      assert.deepEqual(element._parseQueryString(''), []);
-      assert.deepEqual(element._parseQueryString('?'), []);
-      assert.deepEqual(element._parseQueryString('??'), []);
-      assert.deepEqual(element._parseQueryString('&&&'), []);
+      assert.deepEqual(router.parseQueryString(''), []);
+      assert.deepEqual(router.parseQueryString('?'), []);
+      assert.deepEqual(router.parseQueryString('??'), []);
+      assert.deepEqual(router.parseQueryString('&&&'), []);
     });
 
     test('url decoding', () => {
-      assert.deepEqual(element._parseQueryString('+'), [[' ', '']]);
-      assert.deepEqual(element._parseQueryString('???+%3d+'), [[' = ', '']]);
+      assert.deepEqual(router.parseQueryString('+'), [[' ', '']]);
+      assert.deepEqual(router.parseQueryString('???+%3d+'), [[' = ', '']]);
       assert.deepEqual(
-        element._parseQueryString('%6e%61%6d%65=%76%61%6c%75%65'),
+        router.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'), [
+      assert.deepEqual(router.parseQueryString('a=b&c=d&e=f'), [
         ['a', 'b'],
         ['c', 'd'],
         ['e', 'f'],
       ]);
-      assert.deepEqual(element._parseQueryString('&a=b&&&e=f&c'), [
+      assert.deepEqual(router.parseQueryString('&a=b&&&e=f&c'), [
         ['a', 'b'],
         ['e', 'f'],
         ['c', ''],
diff --git a/polygerrit-ui/app/elements/gr-app-element.ts b/polygerrit-ui/app/elements/gr-app-element.ts
index f07daab..bec99663 100644
--- a/polygerrit-ui/app/elements/gr-app-element.ts
+++ b/polygerrit-ui/app/elements/gr-app-element.ts
@@ -26,7 +26,6 @@
 import './core/gr-error-manager/gr-error-manager';
 import './core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog';
 import './core/gr-main-header/gr-main-header';
-import './core/gr-router/gr-router';
 import './core/gr-smart-search/gr-smart-search';
 import './diff/gr-diff-view/gr-diff-view';
 import './edit/gr-editor-view/gr-editor-view';
@@ -95,7 +94,6 @@
 
 export interface GrAppElement {
   $: {
-    router: GrRouter;
     errorManager: GrErrorManager;
     errorView: HTMLDivElement;
     mainHeader: GrMainHeader;
@@ -209,6 +207,8 @@
   @property({type: Boolean})
   _mainAriaHidden = false;
 
+  readonly router = new GrRouter();
+
   private reporting = getAppContext().reportingService;
 
   private readonly restApiService = getAppContext().restApiService;
@@ -263,7 +263,7 @@
     super.ready();
     this._updateLoginUrl();
     this.reporting.appStarted();
-    this.$.router.start();
+    this.router.start();
 
     this.restApiService.getAccount().then(account => {
       this._account = account;
diff --git a/polygerrit-ui/app/elements/gr-app-element_html.ts b/polygerrit-ui/app/elements/gr-app-element_html.ts
index 5b96720..097f559 100644
--- a/polygerrit-ui/app/elements/gr-app-element_html.ts
+++ b/polygerrit-ui/app/elements/gr-app-element_html.ts
@@ -221,7 +221,6 @@
     id="errorManager"
     login-url="[[_loginUrl]]"
   ></gr-error-manager>
-  <gr-router id="router"></gr-router>
   <gr-plugin-host id="plugins" config="[[_serverConfig]]"> </gr-plugin-host>
   <gr-external-style
     id="externalStyleForAll"
diff --git a/polygerrit-ui/app/elements/gr-app_test.ts b/polygerrit-ui/app/elements/gr-app_test.ts
index 02ae6c6..74c4979 100644
--- a/polygerrit-ui/app/elements/gr-app_test.ts
+++ b/polygerrit-ui/app/elements/gr-app_test.ts
@@ -29,6 +29,7 @@
 } from '../test/test-data-generators';
 import {GrAppElement} from './gr-app-element';
 import {GrPluginHost} from './plugins/gr-plugin-host/gr-plugin-host';
+import {GrRouter} from './core/gr-router/gr-router';
 
 suite('gr-app tests', () => {
   let grApp: GrApp;
@@ -39,7 +40,7 @@
   setup(async () => {
     appStartedStub = sinon.stub(getAppContext().reportingService, 'appStarted');
     stub('gr-account-dropdown', '_getTopContent');
-    routerStartStub = stub('gr-router', 'start');
+    routerStartStub = sinon.stub(GrRouter.prototype, 'start');
     stubRestApi('getAccount').returns(Promise.resolve(undefined));
     stubRestApi('getAccountCapabilities').returns(Promise.resolve({}));
     stubRestApi('getConfig').returns(Promise.resolve(config));
diff --git a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.ts b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.ts
index 2eb06a5..fa96059 100644
--- a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.ts
+++ b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.ts
@@ -31,10 +31,7 @@
 import {formStyles} from '../../../styles/gr-form-styles';
 import {when} from 'lit/directives/when';
 import {fire} from '../../../utils/event-util';
-
-type PropertiesOfType<Source, Type> = {
-  [K in keyof Source]: Source[K] extends Type ? K : never;
-}[keyof Source];
+import {PropertiesOfType} from '../../../utils/type-util';
 
 type NotificationKey = PropertiesOfType<Required<ProjectWatchInfo>, boolean>;
 
diff --git a/polygerrit-ui/app/utils/type-util.ts b/polygerrit-ui/app/utils/type-util.ts
new file mode 100644
index 0000000..e91fefc
--- /dev/null
+++ b/polygerrit-ui/app/utils/type-util.ts
@@ -0,0 +1,16 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/**
+ * Gets all properties of a Source that match a given Type. For example:
+ *
+ *   type BooleansOfHTMLElement = PropertiesOfType<HTMLElement, boolean>;
+ *
+ * will be 'draggable' | 'autofocus' | etc.
+ */
+export type PropertiesOfType<Source, Type> = {
+  [K in keyof Source]: Source[K] extends Type ? K : never;
+}[keyof Source];