Merge changes from topic "implement-cached-project-config"

* changes:
  Add serializer for Project
  Implement CachedProjectConfig
  Make AccountsSection an AutoValue
  Make AccessSection, Permission an AutoValue
diff --git a/java/com/google/gerrit/common/data/LabelType.java b/java/com/google/gerrit/common/data/LabelType.java
index 9c1423d..3e58be7 100644
--- a/java/com/google/gerrit/common/data/LabelType.java
+++ b/java/com/google/gerrit/common/data/LabelType.java
@@ -259,6 +259,8 @@
 
     protected abstract String getName();
 
+    protected abstract ImmutableList<Short> getCopyValues();
+
     protected abstract Builder setByValue(ImmutableMap<Short, LabelValue> byValue);
 
     @Nullable
@@ -290,6 +292,8 @@
       }
       setByValue(byValue.build());
 
+      setCopyValues(ImmutableList.sortedCopyOf(getCopyValues()));
+
       return autoBuild();
     }
   }
diff --git a/javatests/com/google/gerrit/acceptance/api/change/BUILD b/javatests/com/google/gerrit/acceptance/api/change/BUILD
index 9279488..f5993a4 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/BUILD
+++ b/javatests/com/google/gerrit/acceptance/api/change/BUILD
@@ -1,11 +1,11 @@
 load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests")
 
-acceptance_tests(
-    srcs = glob(["*IT.java"]),
-    group = "api_change",
+[acceptance_tests(
+    srcs = [f],
+    group = f[:f.index(".")],
     labels = [
         "api",
         "noci",
     ],
     deps = ["//java/com/google/gerrit/server/util/time"],
-)
+) for f in glob(["*IT.java"])]
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/BUILD b/javatests/com/google/gerrit/acceptance/api/revision/BUILD
index 06e45c5..3bfe2f0 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/BUILD
+++ b/javatests/com/google/gerrit/acceptance/api/revision/BUILD
@@ -1,7 +1,7 @@
 load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests")
 
-acceptance_tests(
-    srcs = glob(["*IT.java"]),
-    group = "api_revision",
+[acceptance_tests(
+    srcs = [f],
+    group = f[:f.index(".")],
     labels = ["api"],
-)
+) for f in glob(["*IT.java"])]
diff --git a/javatests/com/google/gerrit/common/data/LabelTypeTest.java b/javatests/com/google/gerrit/common/data/LabelTypeTest.java
index 76ea6e1..2f81fa9 100644
--- a/javatests/com/google/gerrit/common/data/LabelTypeTest.java
+++ b/javatests/com/google/gerrit/common/data/LabelTypeTest.java
@@ -30,6 +30,18 @@
   }
 
   @Test
+  public void sortCopyValues() {
+    LabelValue v0 = LabelValue.create((short) 0, "Zero");
+    LabelValue v1 = LabelValue.create((short) 1, "One");
+    LabelValue v2 = LabelValue.create((short) 2, "Two");
+    LabelType types =
+        LabelType.builder("Label", ImmutableList.of(v2, v0, v1))
+            .setCopyValues(ImmutableList.of((short) 2, (short) 0, (short) 1))
+            .build();
+    assertThat(types.getCopyValues()).containsExactly((short) 0, (short) 1, (short) 2).inOrder();
+  }
+
+  @Test
   public void insertMissingLabelValues() {
     LabelValue v0 = LabelValue.create((short) 0, "Zero");
     LabelValue v2 = LabelValue.create((short) 2, "Two");
diff --git a/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior.js b/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior.js
deleted file mode 100644
index 1d384bc..0000000
--- a/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior.js
+++ /dev/null
@@ -1,48 +0,0 @@
-/**
- * @license
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-/** @polymerBehavior AsyncForeachBehavior */
-export const AsyncForeachBehavior = {
-  /**
-   * @template T
-   * @param {!Array<T>} array
-   * @param {!Function} fn An iteratee function to be passed each element of
-   *     the array in order. Must return a promise, and the following
-   *     iteration will not begin until resolution of the promise returned by
-   *     the previous iteration.
-   *
-   *     An optional second argument to fn is a callback that will halt the
-   *     loop if called.
-   * @return {!Promise<undefined>}
-   */
-  asyncForeach(array, fn) {
-    if (!array.length) { return Promise.resolve(); }
-    let stop = false;
-    const stopCallback = () => { stop = true; };
-    return fn(array[0], stopCallback).then(exit => {
-      if (stop) { return Promise.resolve(); }
-      return this.asyncForeach(array.slice(1), fn);
-    });
-  },
-};
-
-// TODO(dmfilippov) Remove the following lines with assignments
-// Plugins can use the behavior because it was accessible with
-// the global Gerrit... variable. To avoid breaking changes in plugins
-// temporary assign global variables.
-window.Gerrit = window.Gerrit || {};
-window.Gerrit.AsyncForeachBehavior = AsyncForeachBehavior;
diff --git a/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior.ts b/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior.ts
deleted file mode 100644
index 6b726a6..0000000
--- a/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-/**
- * @license
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-// This is a temporary interface. Must be removed when base-url-behavior
-// is converted to a mixin or an util class. See:
-// https://polymer-library.polymer-project.org/3.0/docs/devguide/custom-elements#mixins
-export interface BaseUrlBehaviorInterface {
-  getBaseUrl(): string;
-}
-
-/** @polymerBehavior BaseUrlBehavior */
-export const BaseUrlBehavior = {
-  /** @return {string} */
-  getBaseUrl() {
-    return window.CANONICAL_PATH || '';
-  },
-};
-
-// TODO(dmfilippov) Remove the following lines with assignments
-// Plugins can use the behavior because it was accessible with
-// the global Gerrit... variable. To avoid breaking changes in plugins
-// temporary assign global variables.
-window.Gerrit = window.Gerrit || {};
-window.Gerrit.BaseUrlBehavior = BaseUrlBehavior;
diff --git a/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior_test.js b/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior_test.js
deleted file mode 100644
index 3b8a9cb..0000000
--- a/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior_test.js
+++ /dev/null
@@ -1,51 +0,0 @@
-/**
- * @license
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../../test/common-test-setup-karma.js';
-import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
-import {BaseUrlBehavior} from './base-url-behavior.js';
-
-const basicFixture = fixtureFromElement('base-url-behavior-test-element');
-
-suite('base-url-behavior tests', () => {
-  let element;
-  let originialCanonicalPath;
-
-  suiteSetup(() => {
-    originialCanonicalPath = window.CANONICAL_PATH;
-    window.CANONICAL_PATH = '/r';
-    // Define a Polymer element that uses this behavior.
-    Polymer({
-      is: 'base-url-behavior-test-element',
-      behaviors: [
-        BaseUrlBehavior,
-      ],
-    });
-  });
-
-  suiteTeardown(() => {
-    window.CANONICAL_PATH = originialCanonicalPath;
-  });
-
-  setup(() => {
-    element = basicFixture.instantiate();
-  });
-
-  test('getBaseUrl', () => {
-    assert.deepEqual(element.getBaseUrl(), '/r');
-  });
-});
diff --git a/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior.ts b/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior.ts
deleted file mode 100644
index 4bc2f12..0000000
--- a/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-/**
- * @license
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {
-  BaseUrlBehavior,
-  BaseUrlBehaviorInterface,
-} from '../base-url-behavior/base-url-behavior';
-
-const PROBE_PATH = '/Documentation/index.html';
-const DOCS_BASE_PATH = '/Documentation';
-
-let cachedPromise: Promise<string | null> | undefined;
-
-// NOTE: Below we define 2 types (DocUrlBehaviorConfig and RestApi) to avoid
-// type 'any'. These are temporary definitions and they must be
-// updated/moved/removed when we start converting our codebase to typescript.
-// Right now we are using these types here just for adding typescript support to
-// our build/test infrastructure. Doing so we avoid massive code updates at this
-// stage.
-
-// TODO: introduce global gerrit config type instead of DocUrlBehaviorConfig.
-// The DocUrlBehaviorConfig is a temporary type
-interface DocUrlBehaviorConfig {
-  gerrit?: {doc_url?: string};
-}
-
-// TODO: implement RestApi type correctly and remove interface from this file
-interface RestApi {
-  probePath(url: string): Promise<boolean>;
-}
-
-/** @polymerBehavior DocsUrlBehavior */
-export const DocsUrlBehavior = [
-  {
-    /**
-     * Get the docs base URL from either the server config or by probing.
-     *
-     * @param {Object} config The server config.
-     * @param {!Object} restApi A REST API instance
-     * @return {!Promise<string>} A promise that resolves with the docs base
-     *     URL.
-     */
-    getDocsBaseUrl(
-      this: BaseUrlBehaviorInterface,
-      config: DocUrlBehaviorConfig,
-      restApi: RestApi
-    ) {
-      if (!cachedPromise) {
-        cachedPromise = new Promise(resolve => {
-          if (config && config.gerrit && config.gerrit.doc_url) {
-            resolve(config.gerrit.doc_url);
-          } else {
-            restApi.probePath(this.getBaseUrl() + PROBE_PATH).then(ok => {
-              resolve(ok ? this.getBaseUrl() + DOCS_BASE_PATH : null);
-            });
-          }
-        });
-      }
-      return cachedPromise;
-    },
-
-    /** For testing only. */
-    _clearDocsBaseUrlCache() {
-      cachedPromise = undefined;
-    },
-  },
-  BaseUrlBehavior,
-];
-
-// TODO(dmfilippov) Remove the following lines with assignments
-// Plugins can use the behavior because it was accessible with
-// the global Gerrit... variable. To avoid breaking changes in plugins
-// temporary assign global variables.
-window.Gerrit = window.Gerrit || {};
-window.Gerrit.DocsUrlBehavior = DocsUrlBehavior;
diff --git a/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior_test.js b/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior_test.js
deleted file mode 100644
index 93beb52..0000000
--- a/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior_test.js
+++ /dev/null
@@ -1,89 +0,0 @@
-/**
- * @license
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../../test/common-test-setup-karma.js';
-import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
-import {DocsUrlBehavior} from './docs-url-behavior.js';
-
-const basicFixture = fixtureFromElement('docs-url-behavior-element');
-
-suite('docs-url-behavior tests', () => {
-  let element;
-
-  suiteSetup(() => {
-    // Define a Polymer element that uses this behavior.
-    Polymer({
-      is: 'docs-url-behavior-element',
-      behaviors: [DocsUrlBehavior],
-    });
-  });
-
-  setup(() => {
-    element = basicFixture.instantiate();
-    element._clearDocsBaseUrlCache();
-  });
-
-  test('null config', () => {
-    const mockRestApi = {
-      probePath: sinon.stub().returns(Promise.resolve(true)),
-    };
-    return element.getDocsBaseUrl(null, mockRestApi)
-        .then(docsBaseUrl => {
-          assert.isTrue(
-              mockRestApi.probePath.calledWith('/Documentation/index.html'));
-          assert.equal(docsBaseUrl, '/Documentation');
-        });
-  });
-
-  test('no doc config', () => {
-    const mockRestApi = {
-      probePath: sinon.stub().returns(Promise.resolve(true)),
-    };
-    const config = {gerrit: {}};
-    return element.getDocsBaseUrl(config, mockRestApi)
-        .then(docsBaseUrl => {
-          assert.isTrue(
-              mockRestApi.probePath.calledWith('/Documentation/index.html'));
-          assert.equal(docsBaseUrl, '/Documentation');
-        });
-  });
-
-  test('has doc config', () => {
-    const mockRestApi = {
-      probePath: sinon.stub().returns(Promise.resolve(true)),
-    };
-    const config = {gerrit: {doc_url: 'foobar'}};
-    return element.getDocsBaseUrl(config, mockRestApi)
-        .then(docsBaseUrl => {
-          assert.isFalse(mockRestApi.probePath.called);
-          assert.equal(docsBaseUrl, 'foobar');
-        });
-  });
-
-  test('no probe', () => {
-    const mockRestApi = {
-      probePath: sinon.stub().returns(Promise.resolve(false)),
-    };
-    return element.getDocsBaseUrl(null, mockRestApi)
-        .then(docsBaseUrl => {
-          assert.isTrue(
-              mockRestApi.probePath.calledWith('/Documentation/index.html'));
-          assert.isNotOk(docsBaseUrl);
-        });
-  });
-});
-
diff --git a/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior.js b/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior.js
index 813c64a..180fcc8 100644
--- a/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior.js
+++ b/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior.js
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-import {BaseUrlBehavior} from '../base-url-behavior/base-url-behavior.js';
+import {getBaseUrl} from '../../utils/url-util.js';
 import {URLEncodingBehavior} from '../gr-url-encoding-behavior/gr-url-encoding-behavior.js';
 
 /** @polymerBehavior ListViewBehavior */
@@ -29,7 +29,7 @@
   },
 
   getUrl(path, item) {
-    return this.getBaseUrl() + path + this.encodeURL(item, true);
+    return getBaseUrl() + path + this.encodeURL(item, true);
   },
 
   /**
@@ -52,7 +52,6 @@
     return 0;
   },
 },
-BaseUrlBehavior,
 URLEncodingBehavior,
 ];
 
diff --git a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.ts b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.ts
index 3b30665..5671387 100644
--- a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.ts
+++ b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.ts
@@ -14,10 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {
-  BaseUrlBehavior,
-  BaseUrlBehaviorInterface,
-} from '../base-url-behavior/base-url-behavior';
+import {getBaseUrl} from '../../utils/url-util';
 import {ChangeStatus} from '../../constants/constants';
 
 // WARNING: The types below can be completely wrong!
@@ -131,14 +128,9 @@
     /**
      *  @return {string}
      */
-    changeBaseURL(
-      this: BaseUrlBehaviorInterface,
-      project: string,
-      changeNum: ChangeNum,
-      patchNum: PatchNum
-    ) {
+    changeBaseURL(project: string, changeNum: ChangeNum, patchNum: PatchNum) {
       let v =
-        this.getBaseUrl() +
+        getBaseUrl() +
         '/changes/' +
         encodeURIComponent(project) +
         '~' +
@@ -149,8 +141,8 @@
       return v;
     },
 
-    changePath(this: BaseUrlBehaviorInterface, changeNum: ChangeNum) {
-      return this.getBaseUrl() + '/c/' + changeNum;
+    changePath(changeNum: ChangeNum) {
+      return getBaseUrl() + '/c/' + changeNum;
     },
 
     changeIsOpen(change?: Change) {
@@ -207,7 +199,6 @@
       return this.changeStatuses(change).join(', ');
     },
   },
-  BaseUrlBehavior,
 ];
 
 // eslint-disable-next-line @typescript-eslint/ban-ts-ignore
diff --git a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.js b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.js
index 8f63a25..16f6111 100644
--- a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.js
+++ b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.js
@@ -17,7 +17,6 @@
 
 import '../../test/common-test-setup-karma.js';
 import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
-import {BaseUrlBehavior} from '../base-url-behavior/base-url-behavior.js';
 import {RESTClientBehavior} from './rest-client-behavior.js';
 import {html} from '@polymer/polymer/lib/utils/html-tag.js';
 
@@ -42,7 +41,6 @@
     Polymer({
       is: 'rest-client-behavior-test-element',
       behaviors: [
-        BaseUrlBehavior,
         RESTClientBehavior,
       ],
     });
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
index 25ba83e..5b8896e 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
@@ -38,7 +38,7 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-admin-view_html.js';
-import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import {getBaseUrl} from '../../../utils/url-util.js';
 import {AdminNavBehavior} from '../../../behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.js';
 import {URLEncodingBehavior} from '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
@@ -51,7 +51,6 @@
  */
 class GrAdminView extends mixinBehaviors( [
   AdminNavBehavior,
-  BaseUrlBehavior,
   URLEncodingBehavior,
 ], GestureEventListeners(
     LegacyElementMixin(
@@ -238,7 +237,7 @@
   // updated. They are currently copied from gr-dropdown (and should be
   // updated there as well once complete).
   _computeURLHelper(host, path) {
-    return '//' + host + this.getBaseUrl() + path;
+    return '//' + host + getBaseUrl() + path;
   }
 
   _computeRelativeURL(path) {
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.js b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.js
index 25b14e2..f8b5abd 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.js
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.js
@@ -20,6 +20,7 @@
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
+import {stubBaseUrl} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-admin-view');
 
@@ -50,7 +51,7 @@
         element._computeLinkURL({url: '/test', noBaseUrl: true}),
         '//' + window.location.host + '/test');
 
-    sinon.stub(element, 'getBaseUrl').returns('/foo');
+    stubBaseUrl('/foo');
     assert.equal(
         element._computeLinkURL({url: '/test', noBaseUrl: true}),
         '//' + window.location.host + '/foo/test');
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.js b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.js
index e16f5ce..91573fc 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.js
+++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.js
@@ -26,7 +26,6 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-create-change-dialog_html.js';
-import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
 import {URLEncodingBehavior} from '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 
@@ -37,7 +36,6 @@
  * @extends PolymerElement
  */
 class GrCreateChangeDialog extends mixinBehaviors( [
-  BaseUrlBehavior,
   URLEncodingBehavior,
 ], GestureEventListeners(
     LegacyElementMixin(
diff --git a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.js b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.js
index 7f33663..ea77dd7 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.js
+++ b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.js
@@ -23,7 +23,7 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-create-group-dialog_html.js';
-import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import {getBaseUrl} from '../../../utils/url-util.js';
 import {URLEncodingBehavior} from '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
 import page from 'page/page.mjs';
 
@@ -31,7 +31,6 @@
  * @extends PolymerElement
  */
 class GrCreateGroupDialog extends mixinBehaviors( [
-  BaseUrlBehavior,
   URLEncodingBehavior,
 ], GestureEventListeners(
     LegacyElementMixin(
@@ -63,7 +62,7 @@
   }
 
   _computeGroupUrl(groupId) {
-    return this.getBaseUrl() + '/admin/groups/' +
+    return getBaseUrl() + '/admin/groups/' +
         this.encodeURL(groupId, true);
   }
 
diff --git a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.js b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.js
index eb131f5..635e3b1 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.js
+++ b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.js
@@ -25,7 +25,7 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-create-pointer-dialog_html.js';
-import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import {getBaseUrl} from '../../../utils/url-util.js';
 import {URLEncodingBehavior} from '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
 import page from 'page/page.mjs';
 
@@ -38,7 +38,6 @@
  * @extends PolymerElement
  */
 class GrCreatePointerDialog extends mixinBehaviors( [
-  BaseUrlBehavior,
   URLEncodingBehavior,
 ], GestureEventListeners(
     LegacyElementMixin(
@@ -75,10 +74,10 @@
 
   _computeItemUrl(project) {
     if (this.itemDetail === DETAIL_TYPES.branches) {
-      return this.getBaseUrl() + '/admin/repos/' +
+      return getBaseUrl() + '/admin/repos/' +
           this.encodeURL(this.repoName, true) + ',branches';
     } else if (this.itemDetail === DETAIL_TYPES.tags) {
-      return this.getBaseUrl() + '/admin/repos/' +
+      return getBaseUrl() + '/admin/repos/' +
           this.encodeURL(this.repoName, true) + ',tags';
     }
   }
diff --git a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.js b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.js
index 0410f14..4872a39 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.js
+++ b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.js
@@ -26,7 +26,7 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-create-repo-dialog_html.js';
-import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import {getBaseUrl} from '../../../utils/url-util.js';
 import {URLEncodingBehavior} from '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
 import page from 'page/page.mjs';
 
@@ -34,7 +34,6 @@
  * @extends PolymerElement
  */
 class GrCreateRepoDialog extends mixinBehaviors( [
-  BaseUrlBehavior,
   URLEncodingBehavior,
 ], GestureEventListeners(
     LegacyElementMixin(
@@ -95,7 +94,7 @@
   }
 
   _computeRepoUrl(repoName) {
-    return this.getBaseUrl() + '/admin/repos/' +
+    return getBaseUrl() + '/admin/repos/' +
         this.encodeURL(repoName, true);
   }
 
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js
index c54a709..98985c1 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js
@@ -30,7 +30,7 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-group-members_html.js';
-import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import {getBaseUrl} from '../../../utils/url-util.js';
 import {URLEncodingBehavior} from '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
 
 const SUGGESTIONS_LIMIT = 15;
@@ -43,7 +43,6 @@
  * @extends PolymerElement
  */
 class GrGroupMembers extends mixinBehaviors( [
-  BaseUrlBehavior,
   URLEncodingBehavior,
 ], GestureEventListeners(
     LegacyElementMixin(
@@ -163,9 +162,9 @@
 
     // For GWT compatibility
     if (url.startsWith('#')) {
-      return this.getBaseUrl() + url.slice(1);
+      return getBaseUrl() + url.slice(1);
     }
-    return this.getBaseUrl() + url;
+    return getBaseUrl() + url;
   }
 
   _handleSavingGroupMember() {
@@ -230,16 +229,19 @@
 
   _handleSavingIncludedGroups() {
     return this.$.restAPI.saveIncludedGroup(this._groupName,
-        this._includedGroupSearchId.replace(/\+/g, ' '), err => {
-          if (err.status === 404) {
-            this.dispatchEvent(new CustomEvent('show-alert', {
-              detail: {message: SAVING_ERROR_TEXT},
-              bubbles: true,
-              composed: true,
-            }));
-            return err;
+        this._includedGroupSearchId.replace(/\+/g, ' '), (errResponse, err) => {
+          if (errResponse) {
+            if (errResponse.status === 404) {
+              this.dispatchEvent(new CustomEvent('show-alert', {
+                detail: {message: SAVING_ERROR_TEXT},
+                bubbles: true,
+                composed: true,
+              }));
+              return errResponse;
+            }
+            throw Error(err.statusText);
           }
-          throw Error(err.statusText);
+          throw err;
         })
         .then(config => {
           if (!config) {
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.js b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.js
index f047cfe..3e3e572 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.js
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.js
@@ -18,6 +18,7 @@
 import '../../../test/common-test-setup-karma.js';
 import './gr-group-members.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {stubBaseUrl} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-group-members');
 
@@ -135,7 +136,7 @@
       },
     });
     element = basicFixture.instantiate();
-    sinon.stub(element, 'getBaseUrl').returns('https://test/site');
+    stubBaseUrl('https://test/site');
     element.groupId = 1;
     groupStub = sinon.stub(
         element.$.restAPI,
@@ -213,10 +214,12 @@
     const memberName = 'bad-name';
     const alertStub = sinon.stub();
     element.addEventListener('show-alert', alertStub);
-    const error = new Error('error');
-    error.status = 404;
-    sinon.stub(element.$.restAPI, 'saveGroupMembers').callsFake(
-        () => Promise.reject(error));
+    const errorResponse = {
+      status: 404,
+      ok: false,
+    };
+    sinon.stub(element.$.restAPI._restApiHelper, 'fetch').callsFake(
+        () => Promise.resolve(errorResponse));
 
     element.$.groupMemberSearchInput.text = memberName;
     element.$.groupMemberSearchInput.value = 1234;
@@ -226,6 +229,28 @@
     });
   });
 
+  test('add included group network-error throws an exception', async () => {
+    element._groupOwner = true;
+
+    const memberName = 'bad-name';
+    const alertStub = sinon.stub();
+    element.addEventListener('show-alert', alertStub);
+    const err = new Error();
+    sinon.stub(element.$.restAPI._restApiHelper, 'fetch').callsFake(
+        () => Promise.reject(err));
+
+    element.$.groupMemberSearchInput.text = memberName;
+    element.$.groupMemberSearchInput.value = 1234;
+
+    let exceptionThrown = false;
+    try {
+      await element._handleSavingIncludedGroups();
+    } catch (e) {
+      exceptionThrown = true;
+    }
+    assert.isTrue(exceptionThrown);
+  });
+
   test('_getAccountSuggestions empty', done => {
     element
         ._getAccountSuggestions('nonexistent').then(accounts => {
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js
index f6a1c10..8308504 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js
@@ -25,7 +25,7 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-repo-access_html.js';
-import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import {getBaseUrl} from '../../../utils/url-util.js';
 import {AccessBehavior} from '../../../behaviors/gr-access-behavior/gr-access-behavior.js';
 import {URLEncodingBehavior} from '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
@@ -85,7 +85,6 @@
  */
 class GrRepoAccess extends mixinBehaviors( [
   AccessBehavior,
-  BaseUrlBehavior,
   URLEncodingBehavior,
 ], GestureEventListeners(
     LegacyElementMixin(
@@ -514,7 +513,7 @@
   }
 
   _computeParentHref(repoName) {
-    return this.getBaseUrl() +
+    return getBaseUrl() +
         `/admin/repos/${this.encodeURL(repoName, true)},access`;
   }
 }
diff --git a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js
index 99957ff..f50b9ed 100644
--- a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js
+++ b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js
@@ -25,7 +25,7 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-rule-editor_html.js';
-import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import {getBaseUrl} from '../../../utils/url-util.js';
 import {AccessBehavior} from '../../../behaviors/gr-access-behavior/gr-access-behavior.js';
 import {URLEncodingBehavior} from '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
 
@@ -81,7 +81,6 @@
  */
 class GrRuleEditor extends mixinBehaviors( [
   AccessBehavior,
-  BaseUrlBehavior,
   URLEncodingBehavior,
 ], GestureEventListeners(
     LegacyElementMixin(
@@ -172,7 +171,7 @@
   }
 
   _computeGroupPath(group) {
-    return `${this.getBaseUrl()}/admin/groups/${this.encodeURL(group, true)}`;
+    return `${getBaseUrl()}/admin/groups/${this.encodeURL(group, true)}`;
   }
 
   _handleAccessSaved() {
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
index 6bb7f0c..1e06795 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
@@ -31,7 +31,6 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-change-list-item_html.js';
-import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
 import {ChangeTableBehavior} from '../../../behaviors/gr-change-table-behavior/gr-change-table-behavior.js';
 import {PathListBehavior} from '../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.js';
 import {URLEncodingBehavior} from '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
@@ -57,7 +56,6 @@
  * @extends PolymerElement
  */
 class GrChangeListItem extends mixinBehaviors( [
-  BaseUrlBehavior,
   ChangeTableBehavior,
   PathListBehavior,
   RESTClientBehavior,
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
index 361ad83..baaf428 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
@@ -26,7 +26,6 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-change-list-view_html.js';
-import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
 import {URLEncodingBehavior} from '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
 import page from 'page/page.mjs';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
@@ -47,7 +46,6 @@
  * @extends PolymerElement
  */
 class GrChangeListView extends mixinBehaviors( [
-  BaseUrlBehavior,
   URLEncodingBehavior,
 ], GestureEventListeners(
     LegacyElementMixin(
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
index b238689..f873802 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
@@ -29,7 +29,6 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-change-list_html.js';
 import {appContext} from '../../../services/app-context.js';
-import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
 import {ChangeTableBehavior} from '../../../behaviors/gr-change-table-behavior/gr-change-table-behavior.js';
 import {URLEncodingBehavior} from '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
 import {KeyboardShortcutBehavior} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
@@ -47,7 +46,6 @@
  * @extends PolymerElement
  */
 class GrChangeList extends mixinBehaviors( [
-  BaseUrlBehavior,
   ChangeTableBehavior,
   KeyboardShortcutBehavior,
   RESTClientBehavior,
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index af542e0..8f27730 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -1354,15 +1354,12 @@
     if ([changeRecord, canStartReview].includes(undefined)) {
       return 'Reply';
     }
-    if (canStartReview) {
-      return 'Start Review';
-    }
 
     const drafts = (changeRecord && changeRecord.base) || {};
     const draftCount = Object.keys(drafts)
         .reduce((count, file) => count + drafts[file].length, 0);
 
-    let label = 'Reply';
+    let label = canStartReview ? 'Start Review' : 'Reply';
     if (draftCount > 0) {
       label += ' (' + draftCount + ')';
     }
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.js
index d2e8ea7..c8b13b9 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.js
@@ -1136,6 +1136,7 @@
       'file2.txt': [{}, {}],
     };
     assert.equal(getLabel(changeRecord, false), 'Reply (3)');
+    assert.equal(getLabel(changeRecord, true), 'Start Review (3)');
   });
 
   test('comment events properly update diff drafts', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
index 44700e9..df6490d 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -33,7 +33,7 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-file-list_html.js';
-import {AsyncForeachBehavior} from '../../../behaviors/async-foreach-behavior/async-foreach-behavior.js';
+import {asyncForeach} from '../../../utils/async-util.js';
 import {DomUtilBehavior} from '../../../behaviors/dom-util-behavior/dom-util-behavior.js';
 import {PatchSetBehavior} from '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
 import {PathListBehavior} from '../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.js';
@@ -91,7 +91,6 @@
  * @extends PolymerElement
  */
 class GrFileList extends mixinBehaviors( [
-  AsyncForeachBehavior,
   DomUtilBehavior,
   KeyboardShortcutBehavior,
   PatchSetBehavior,
@@ -1292,7 +1291,7 @@
         detail: {resolve},
         composed: true, bubbles: true,
       }));
-    })).then(() => this.asyncForeach(files, (file, cancel) => {
+    })).then(() => asyncForeach(files, (file, cancel) => {
       const path = file.path;
       this._cancelForEachDiff = cancel;
 
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
index 0690a87..d5416b2 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
@@ -33,7 +33,6 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-reply-dialog_html.js';
-import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
 import {PatchSetBehavior} from '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
 import {KeyboardShortcutBehavior} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
 import {RESTClientBehavior} from '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
@@ -82,7 +81,6 @@
  * @extends PolymerElement
  */
 class GrReplyDialog extends mixinBehaviors( [
-  BaseUrlBehavior,
   KeyboardShortcutBehavior,
   PatchSetBehavior,
   RESTClientBehavior,
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
index a6aef95..7e4e568 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
@@ -21,12 +21,11 @@
 import '../../shared/gr-overlay/gr-overlay.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
 import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
-import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-error-manager_html.js';
-import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import {getBaseUrl} from '../../../utils/url-util.js';
 import {authService} from '../../shared/gr-rest-api-interface/gr-auth.js';
 import {appContext} from '../../../services/app-context.js';
 
@@ -41,11 +40,9 @@
 /**
  * @extends PolymerElement
  */
-class GrErrorManager extends mixinBehaviors( [
-  BaseUrlBehavior,
-], GestureEventListeners(
+class GrErrorManager extends GestureEventListeners(
     LegacyElementMixin(
-        PolymerElement))) {
+        PolymerElement)) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-error-manager'; }
@@ -372,7 +369,7 @@
       'left=' + left,
       'top=' + top,
     ];
-    window.open(this.getBaseUrl() +
+    window.open(getBaseUrl() +
         '/login/%3FcloseAfterLogin', '_blank', options.join(','));
     this.listen(window, 'focus', '_handleWindowFocus');
   }
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
index 8bc1870..17a7b0b 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
@@ -26,8 +26,7 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-main-header_html.js';
-import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
-import {DocsUrlBehavior} from '../../../behaviors/docs-url-behavior/docs-url-behavior.js';
+import {getBaseUrl, getDocsBaseUrl} from '../../../utils/url-util.js';
 import {AdminNavBehavior} from '../../../behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.js';
 import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
 
@@ -88,8 +87,6 @@
  */
 class GrMainHeader extends mixinBehaviors( [
   AdminNavBehavior,
-  BaseUrlBehavior,
-  DocsUrlBehavior,
 ], GestureEventListeners(
     LegacyElementMixin(
         PolymerElement))) {
@@ -185,7 +182,7 @@
   }
 
   _computeRelativeURL(path) {
-    return '//' + window.location.host + this.getBaseUrl() + path;
+    return '//' + window.location.host + getBaseUrl() + path;
   }
 
   _computeLinks(defaultLinks, userLinks, adminLinks, topMenus, docBaseUrl) {
@@ -288,7 +285,7 @@
     this.$.restAPI.getConfig()
         .then(config => {
           this._retrieveRegisterURL(config);
-          return this.getDocsBaseUrl(config, this.$.restAPI);
+          return getDocsBaseUrl(config, this.$.restAPI);
         })
         .then(docBaseUrl => { this._docBaseUrl = docBaseUrl; });
   }
@@ -334,7 +331,7 @@
   }
 
   _generateSettingsLink() {
-    return this.getBaseUrl() + '/settings/';
+    return getBaseUrl() + '/settings/';
   }
 
   _onMobileSearchTap(e) {
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.js b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
index b60f47e..83c91e0 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -21,7 +21,7 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import page from 'page/page.mjs';
 import {htmlTemplate} from './gr-router_html.js';
-import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import {getBaseUrl} from '../../../utils/url-util.js';
 import {PatchSetBehavior} from '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
 import {URLEncodingBehavior} from '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
 import {GerritNav} from '../gr-navigation/gr-navigation.js';
@@ -217,7 +217,6 @@
  * @extends PolymerElement
  */
 class GrRouter extends mixinBehaviors( [
-  BaseUrlBehavior,
   PatchSetBehavior,
   URLEncodingBehavior,
 ], GestureEventListeners(
@@ -277,7 +276,7 @@
    * @return {string}
    */
   _generateUrl(params) {
-    const base = this.getBaseUrl();
+    const base = getBaseUrl();
     let url = '';
     const Views = GerritNav.View;
 
@@ -627,7 +626,7 @@
    * @param {string} returnUrl
    */
   _redirectToLogin(returnUrl) {
-    const basePath = this.getBaseUrl() || '';
+    const basePath = getBaseUrl() || '';
     page(
         '/login/' + encodeURIComponent(returnUrl.substring(basePath.length)));
   }
@@ -725,7 +724,7 @@
   }
 
   _startRouter() {
-    const base = this.getBaseUrl();
+    const base = getBaseUrl();
     if (base) {
       page.base(base);
     }
@@ -947,7 +946,7 @@
         // See Issue 6888.
         hash = hash.replace('/ /', '/+/');
       }
-      const base = this.getBaseUrl();
+      const base = getBaseUrl();
       let newUrl = base + hash;
       if (hash.startsWith('/VE/')) {
         newUrl = base + '/settings' + hash;
@@ -1492,7 +1491,7 @@
     if (path.startsWith('/register')) { path = '/'; }
 
     if (path[0] !== '/') { return; }
-    this._redirect(this.getBaseUrl() + path);
+    this._redirect(getBaseUrl() + path);
   }
 
   /**
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.js b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.js
index b53b269..7e36823 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.js
@@ -19,6 +19,7 @@
 import './gr-router.js';
 import page from 'page/page.mjs';
 import {GerritNav} from '../gr-navigation/gr-navigation.js';
+import {stubBaseUrl} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-router');
 
@@ -838,7 +839,7 @@
             querystring: '',
             hash: '/foo/bar',
           };
-          sinon.stub(element, 'getBaseUrl').returns('/baz');
+          stubBaseUrl('/baz');
           const result = element._handleRootRoute(data);
           assert.isNotOk(result);
           assert.isTrue(redirectStub.called);
diff --git a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.js b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.js
index 01268b1..352f1d8 100644
--- a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.js
+++ b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.js
@@ -24,6 +24,7 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-documentation-search_html.js';
 import {ListViewBehavior} from '../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.js';
+import {getBaseUrl} from '../../../utils/url-util.js';
 
 /**
  * @extends PolymerElement
@@ -92,7 +93,7 @@
 
   _computeSearchUrl(url) {
     if (!url) { return ''; }
-    return this.getBaseUrl() + '/' + url;
+    return getBaseUrl() + '/' + url;
   }
 }
 
diff --git a/polygerrit-ui/app/elements/gr-app-element.js b/polygerrit-ui/app/elements/gr-app-element.js
index db098c5..63a7e9c 100644
--- a/polygerrit-ui/app/elements/gr-app-element.js
+++ b/polygerrit-ui/app/elements/gr-app-element.js
@@ -45,7 +45,7 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-app-element_html.js';
-import {BaseUrlBehavior} from '../behaviors/base-url-behavior/base-url-behavior.js';
+import {getBaseUrl} from '../utils/url-util.js';
 import {KeyboardShortcutBehavior} from '../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
 import {GerritNav} from './core/gr-navigation/gr-navigation.js';
 import {appContext} from '../services/app-context.js';
@@ -54,7 +54,6 @@
  * @extends PolymerElement
  */
 class GrAppElement extends mixinBehaviors( [
-  BaseUrlBehavior,
   KeyboardShortcutBehavior,
 ], GestureEventListeners(
     LegacyElementMixin(
@@ -477,7 +476,7 @@
   }
 
   _updateLoginUrl() {
-    const baseUrl = this.getBaseUrl();
+    const baseUrl = getBaseUrl();
     if (baseUrl) {
       // Strip the canonical path from the path since needing canonical in
       // the path is unneeded and breaks the url.
diff --git a/polygerrit-ui/app/elements/gr-app-global-var-init.js b/polygerrit-ui/app/elements/gr-app-global-var-init.js
index 8c1161c..9961971 100644
--- a/polygerrit-ui/app/elements/gr-app-global-var-init.js
+++ b/polygerrit-ui/app/elements/gr-app-global-var-init.js
@@ -64,7 +64,8 @@
 import {GrStylesApi} from './plugins/gr-styles-api/gr-styles-api.js';
 import {pluginLoader, PluginLoader} from './shared/gr-js-api-interface/gr-plugin-loader.js';
 import {GrPluginActionContext} from './shared/gr-js-api-interface/gr-plugin-action-context.js';
-import {getBaseUrl, getPluginNameFromUrl, getRestAPI, PLUGIN_LOADING_TIMEOUT_MS, PRELOADED_PROTOCOL, send} from './shared/gr-js-api-interface/gr-api-utils.js';
+import {getPluginNameFromUrl, getRestAPI, PLUGIN_LOADING_TIMEOUT_MS, PRELOADED_PROTOCOL, send} from './shared/gr-js-api-interface/gr-api-utils.js';
+import {getBaseUrl} from '../utils/url-util.js';
 import {GerritNav} from './core/gr-navigation/gr-navigation.js';
 import {getRootElement} from '../scripts/rootElement.js';
 import {rangesEqual} from './diff/gr-diff/gr-diff-utils.js';
diff --git a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.js b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.js
index 0a84f56c..e0da53d 100644
--- a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.js
+++ b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.js
@@ -18,21 +18,18 @@
 import '../../../styles/gr-form-styles.js';
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
-import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-agreements-list_html.js';
-import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import {getBaseUrl} from '../../../utils/url-util.js';
 
 /**
  * @extends PolymerElement
  */
-class GrAgreementsList extends mixinBehaviors( [
-  BaseUrlBehavior,
-], GestureEventListeners(
+class GrAgreementsList extends GestureEventListeners(
     LegacyElementMixin(
-        PolymerElement))) {
+        PolymerElement)) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-agreements-list'; }
@@ -56,11 +53,11 @@
   }
 
   getUrl() {
-    return this.getBaseUrl() + '/settings/new-agreement';
+    return getBaseUrl() + '/settings/new-agreement';
   }
 
   getUrlBase(item) {
-    return this.getBaseUrl() + '/' + item;
+    return getBaseUrl() + '/' + item;
   }
 }
 
diff --git a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.js b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.js
index 8359f96..023eee8 100644
--- a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.js
+++ b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.js
@@ -20,21 +20,18 @@
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-button/gr-button.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
-import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-cla-view_html.js';
-import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import {getBaseUrl} from '../../../utils/url-util.js';
 
 /**
  * @extends PolymerElement
  */
-class GrClaView extends mixinBehaviors( [
-  BaseUrlBehavior,
-], GestureEventListeners(
+class GrClaView extends GestureEventListeners(
     LegacyElementMixin(
-        PolymerElement))) {
+        PolymerElement)) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-cla-view'; }
@@ -91,7 +88,7 @@
     if (configUrl.startsWith('http:') || configUrl.startsWith('https:')) {
       url = configUrl;
     } else {
-      url = this.getBaseUrl() + '/' + configUrl;
+      url = getBaseUrl() + '/' + configUrl;
     }
 
     return url;
diff --git a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.js b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.js
index d0d30ea..eee06e3 100644
--- a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.js
+++ b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.js
@@ -20,12 +20,11 @@
 import '../../shared/gr-button/gr-button.js';
 import '../../shared/gr-overlay/gr-overlay.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
-import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-identities_html.js';
-import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import {getBaseUrl} from '../../../utils/url-util.js';
 
 const AUTH = [
   'OPENID',
@@ -35,11 +34,9 @@
 /**
  * @extends PolymerElement
  */
-class GrIdentities extends mixinBehaviors( [
-  BaseUrlBehavior,
-], GestureEventListeners(
+class GrIdentities extends GestureEventListeners(
     LegacyElementMixin(
-        PolymerElement))) {
+        PolymerElement)) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-identities'; }
@@ -106,7 +103,7 @@
   }
 
   _computeLinkAnotherIdentity() {
-    const baseUrl = this.getBaseUrl() || '';
+    const baseUrl = getBaseUrl() || '';
     let pathname = window.location.pathname;
     if (baseUrl) {
       pathname = '/' + pathname.substring(baseUrl.length);
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
index 3b889c4..c1ca460 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
@@ -45,7 +45,7 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-settings-view_html.js';
-import {DocsUrlBehavior} from '../../../behaviors/docs-url-behavior/docs-url-behavior.js';
+import {getDocsBaseUrl} from '../../../utils/url-util.js';
 import {ChangeTableBehavior} from '../../../behaviors/gr-change-table-behavior/gr-change-table-behavior.js';
 
 const PREFS_SECTION_FIELDS = [
@@ -78,7 +78,6 @@
  * @extends PolymerElement
  */
 class GrSettingsView extends mixinBehaviors( [
-  DocsUrlBehavior,
   ChangeTableBehavior,
 ], GestureEventListeners(
     LegacyElementMixin(
@@ -237,7 +236,7 @@
       }
 
       configPromises.push(
-          this.getDocsBaseUrl(config, this.$.restAPI)
+          getDocsBaseUrl(config, this.$.restAPI)
               .then(baseUrl => { this._docsBaseUrl = baseUrl; }));
 
       return Promise.all(configPromises);
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js
index 3844bea..eff1953 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js
@@ -17,22 +17,18 @@
 
 import '../gr-account-label/gr-account-label.js';
 import '../../../styles/shared-styles.js';
-import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-account-link_html.js';
-import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 
 /**
  * @extends PolymerElement
  */
-class GrAccountLink extends mixinBehaviors( [
-  BaseUrlBehavior,
-], GestureEventListeners(
+class GrAccountLink extends GestureEventListeners(
     LegacyElementMixin(
-        PolymerElement))) {
+        PolymerElement)) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-account-link'; }
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
index 1385f6d..0d30179 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
@@ -17,22 +17,19 @@
 import '../../../styles/shared-styles.js';
 import '../gr-js-api-interface/gr-js-api-interface.js';
 import '../gr-rest-api-interface/gr-rest-api-interface.js';
-import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-avatar_html.js';
-import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import {getBaseUrl} from '../../../utils/url-util.js';
 import {pluginLoader} from '../gr-js-api-interface/gr-plugin-loader.js';
 
 /**
  * @extends PolymerElement
  */
-class GrAvatar extends mixinBehaviors( [
-  BaseUrlBehavior,
-], GestureEventListeners(
+class GrAvatar extends GestureEventListeners(
     LegacyElementMixin(
-        PolymerElement))) {
+        PolymerElement)) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-avatar'; }
@@ -101,7 +98,7 @@
         return avatars[i].url;
       }
     }
-    return this.getBaseUrl() + '/accounts/' +
+    return getBaseUrl() + '/accounts/' +
       encodeURIComponent(this._getAccounts(account)) +
       '/avatar?s=' + this.imageSize;
   }
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
index 717275d..0828bf6 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
@@ -26,7 +26,7 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-dropdown_html.js';
-import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import {getBaseUrl} from '../../../utils/url-util.js';
 import {KeyboardShortcutBehavior} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
 
 const REL_NOOPENER = 'noopener';
@@ -36,7 +36,6 @@
  * @extends PolymerElement
  */
 class GrDropdown extends mixinBehaviors( [
-  BaseUrlBehavior,
   KeyboardShortcutBehavior,
 ], GestureEventListeners(
     LegacyElementMixin(
@@ -233,8 +232,8 @@
    * @return {!string} The scheme-relative URL.
    */
   _computeURLHelper(host, path) {
-    const base = path.startsWith(this.getBaseUrl()) ?
-      '' : this.getBaseUrl();
+    const base = path.startsWith(getBaseUrl()) ?
+      '' : getBaseUrl();
     return '//' + host + base + path;
   }
 
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils.js
index 81ece81..9bef3a2 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils.js
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import {getBaseUrl} from '../../../utils/url-util.js';
 
 export const PRELOADED_PROTOCOL = 'preloaded:';
 export const PLUGIN_LOADING_TIMEOUT_MS = 10000;
@@ -28,10 +28,6 @@
   return _restAPI;
 }
 
-export function getBaseUrl() {
-  return BaseUrlBehavior.getBaseUrl();
-}
-
 /**
  * Retrieves the name of the plugin base on the url.
  *
@@ -49,7 +45,7 @@
   if (url.protocol === PRELOADED_PROTOCOL) {
     return url.pathname;
   }
-  const base = BaseUrlBehavior.getBaseUrl();
+  const base = getBaseUrl();
   let pathname = url.pathname.replace(base, '');
   // Load from ASSETS_PATH
   if (window.ASSETS_PATH && url.href.includes(window.ASSETS_PATH)) {
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.js
index 69d635b..946325f 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.js
@@ -17,13 +17,13 @@
 
 import '../../../test/common-test-setup-karma.js';
 import './gr-js-api-interface.js';
-import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
 import {GrPopupInterface} from '../../plugins/gr-popup-interface/gr-popup-interface.js';
 import {GrSettingsApi} from '../../plugins/gr-settings-api/gr-settings-api.js';
 import {GrPluginActionContext} from './gr-plugin-action-context.js';
 import {PLUGIN_LOADING_TIMEOUT_MS} from './gr-api-utils.js';
 import {pluginLoader} from './gr-plugin-loader.js';
 import {_testOnly_initGerritPluginApi} from './gr-gerrit.js';
+import {stubBaseUrl} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-js-api-interface');
 
@@ -371,7 +371,7 @@
     let baseUrlPlugin;
 
     setup(() => {
-      sinon.stub(BaseUrlBehavior, 'getBaseUrl').returns('/r');
+      stubBaseUrl('/r');
 
       pluginApi.install(p => { baseUrlPlugin = p; }, '0.1',
           'http://test.com/r/plugins/baseurlplugin/static/test.js');
@@ -460,7 +460,7 @@
 
   suite('screen', () => {
     test('screenUrl()', () => {
-      sinon.stub(BaseUrlBehavior, 'getBaseUrl').returns('/base');
+      stubBaseUrl('/base');
       assert.equal(
           plugin.screenUrl(),
           `${location.origin}/base/x/testplugin`
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.js
index fed91de..0bf49c3 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.js
@@ -23,9 +23,10 @@
   PLUGIN_LOADING_TIMEOUT_MS,
   PRELOADED_PROTOCOL,
   getPluginNameFromUrl,
-  getBaseUrl,
 } from './gr-api-utils.js';
 
+import {getBaseUrl} from '../../../utils/url-util.js';
+
 /**
  * @enum {string}
  */
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.js
index e8839d6..1e6938e 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.js
@@ -17,10 +17,9 @@
 
 import '../../../test/common-test-setup-karma.js';
 import './gr-js-api-interface.js';
-import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
 import {PRELOADED_PROTOCOL, PLUGIN_LOADING_TIMEOUT_MS} from './gr-api-utils.js';
 import {_testOnly_resetPluginLoader} from './gr-plugin-loader.js';
-import {resetPlugins} from '../../../test/test-utils.js';
+import {resetPlugins, stubBaseUrl} from '../../../test/test-utils.js';
 import {_testOnly_flushPreinstalls} from './gr-gerrit.js';
 import {_testOnly_initGerritPluginApi} from './gr-gerrit.js';
 
@@ -348,7 +347,7 @@
 
     test('relative path should honor getBaseUrl', () => {
       const testUrl = '/test';
-      sinon.stub(BaseUrlBehavior, 'getBaseUrl').callsFake(() => testUrl);
+      stubBaseUrl(testUrl);
 
       pluginLoader.loadPlugins([
         'foo/bar.js',
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
index 9d79462..446ceb5 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import {getBaseUrl} from '../../../utils/url-util.js';
 import {GrAttributeHelper} from '../../plugins/gr-attribute-helper/gr-attribute-helper.js';
 import {GrChangeActionsInterface} from './gr-change-actions-js-api.js';
 import {GrChangeReplyInterface} from './gr-change-reply-js-api.js';
@@ -152,7 +152,7 @@
   Plugin.prototype.url = function(opt_path) {
     const relPath = '/plugins/' + this._name + (opt_path || '/');
     const sameOriginPath = window.location.origin +
-      `${BaseUrlBehavior.getBaseUrl()}${relPath}`;
+      `${getBaseUrl()}${relPath}`;
     if (window.location.origin === this._url.origin) {
       // Plugin loaded from the same origin as gr-app, getBaseUrl in effect.
       return sameOriginPath;
@@ -168,7 +168,7 @@
 
   Plugin.prototype.screenUrl = function(opt_screenName) {
     const origin = location.origin;
-    const base = BaseUrlBehavior.getBaseUrl();
+    const base = getBaseUrl();
     const tokenPart = opt_screenName ? '/' + opt_screenName : '';
     return `${origin}${base}/x/${this.getPluginName()}${tokenPart}`;
   };
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js b/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js
index 6f8a88a..f49cf0f 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import {getBaseUrl} from '../../../utils/url-util.js';
 
 /**
  * Pattern describing URLs with supported protocols.
@@ -43,7 +43,7 @@
   this.linkConfig = linkConfig;
   this.callback = callback;
   this.removeZeroWidthSpace = opt_removeZeroWidthSpace;
-  this.baseUrl = BaseUrlBehavior.getBaseUrl();
+  this.baseUrl = getBaseUrl();
   Object.preventExtensions(this);
 }
 
diff --git a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js
index 595694c..6e8b3a3 100644
--- a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js
+++ b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js
@@ -23,7 +23,7 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-list-view_html.js';
-import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import {getBaseUrl} from '../../../utils/url-util.js';
 import {URLEncodingBehavior} from '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
 import page from 'page/page.mjs';
 
@@ -33,7 +33,6 @@
  * @extends PolymerElement
  */
 class GrListView extends mixinBehaviors( [
-  BaseUrlBehavior,
   URLEncodingBehavior,
 ], GestureEventListeners(
     LegacyElementMixin(
@@ -91,7 +90,7 @@
     // Offset could be a string when passed from the router.
     offset = +(offset || 0);
     const newOffset = Math.max(0, offset + (itemsPerPage * direction));
-    let href = this.getBaseUrl() + path;
+    let href = getBaseUrl() + path;
     if (filter) {
       href += '/q/filter:' + this.encodeURL(filter, false);
     }
diff --git a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.js b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.js
index 93451ff..7782629 100644
--- a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.js
@@ -18,6 +18,7 @@
 import '../../../test/common-test-setup-karma.js';
 import './gr-list-view.js';
 import page from 'page/page.mjs';
+import {stubBaseUrl} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-list-view');
 
@@ -34,7 +35,7 @@
     let filter = 'test';
     const path = '/admin/projects';
 
-    sinon.stub(element, 'getBaseUrl').callsFake(() => '');
+    stubBaseUrl('');
 
     assert.equal(
         element._computeNavLink(offset, 1, projectsPerPage, filter, path),
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth.js
index 50a837d..5fcd1a4 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth.js
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import {getBaseUrl} from '../../../utils/url-util.js';
 import {appContext} from '../../../services/app-context.js';
 
 const MAX_AUTH_CHECK_WAIT_TIME_MS = 1000 * 30; // 30s
@@ -38,7 +38,7 @@
   }
 
   get baseUrl() {
-    return BaseUrlBehavior.getBaseUrl();
+    return getBaseUrl();
   }
 
   /**
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.js
index af2efae..efdd4d1 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.js
@@ -16,9 +16,9 @@
  */
 
 import '../../../test/common-test-setup-karma.js';
-import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
 import {Auth, authService} from './gr-auth.js';
 import {appContext} from '../../../services/app-context.js';
+import {stubBaseUrl} from '../../../test/test-utils.js';
 
 suite('gr-auth', () => {
   let auth;
@@ -260,7 +260,7 @@
 
     test('base url support', done => {
       const baseUrl = 'http://foo';
-      sinon.stub(BaseUrlBehavior, 'getBaseUrl').returns(baseUrl);
+      stubBaseUrl(baseUrl);
       auth.fetch(baseUrl + '/url', {bar: 'bar'}).then(() => {
         const [url] = fetch.lastCall.args;
         assert.equal(url, 'http://foo/a/url?access_token=zbaz');
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index 3c76ee1..60f1463 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -28,6 +28,7 @@
 import {GrReviewerUpdatesParser} from './gr-reviewer-updates-parser.js';
 import {parseDate} from '../../../utils/date-util.js';
 import {authService} from './gr-auth.js';
+import {getBaseUrl} from '../../../utils/url-util.js';
 
 const DiffViewMode = {
   SIDE_BY_SIDE: 'SIDE_BY_SIDE',
@@ -2271,7 +2272,7 @@
   }
 
   _fetchB64File(url) {
-    return this._restApiHelper.fetch({url: this.getBaseUrl() + url})
+    return this._restApiHelper.fetch({url: getBaseUrl() + url})
         .then(response => {
           if (!response.ok) {
             return Promise.reject(new Error(response.statusText));
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.js
index bc70791..d54d342 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.js
@@ -14,6 +14,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import {getBaseUrl} from '../../../../utils/url-util.js';
+
 const JSON_PREFIX = ')]}\'';
 
 /**
@@ -237,7 +239,7 @@
    * @return {string}
    */
   urlWithParams(url, opt_params) {
-    if (!opt_params) { return this.getBaseUrl() + url; }
+    if (!opt_params) { return getBaseUrl() + url; }
 
     const params = [];
     for (const p in opt_params) {
@@ -250,7 +252,7 @@
         params.push(`${encodeURIComponent(p)}=${encodeURIComponent(value)}`);
       }
     }
-    return this.getBaseUrl() + url + '?' + params.join('&');
+    return getBaseUrl() + url + '?' + params.join('&');
   }
 
   /**
@@ -299,10 +301,6 @@
     return req;
   }
 
-  getBaseUrl() {
-    return this._restApiInterface.getBaseUrl();
-  }
-
   dispatchEvent(type, detail) {
     return this._restApiInterface.dispatchEvent(type, detail);
   }
@@ -358,7 +356,7 @@
       }
     }
     const url = req.url.startsWith('http') ?
-      req.url : this.getBaseUrl() + req.url;
+      req.url : getBaseUrl() + req.url;
     const fetchReq = {
       url,
       fetchOptions: options,
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.js
index 8acba73..9cac375 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.js
@@ -35,7 +35,6 @@
     window.CANONICAL_PATH = 'testhelper';
 
     const mockRestApiInterface = {
-      getBaseUrl: sinon.stub().returns(window.CANONICAL_PATH),
       fire: sinon.stub(),
     };
 
diff --git a/polygerrit-ui/app/test/common-test-setup.js b/polygerrit-ui/app/test/common-test-setup.js
index 36e27ba..9ac05f9 100644
--- a/polygerrit-ui/app/test/common-test-setup.js
+++ b/polygerrit-ui/app/test/common-test-setup.js
@@ -26,7 +26,7 @@
 import {_testOnly_resetPluginLoader} from '../elements/shared/gr-js-api-interface/gr-plugin-loader.js';
 import {_testOnlyResetRestApi} from '../elements/shared/gr-js-api-interface/gr-plugin-rest-api.js';
 import {_testOnlyResetGrRestApiSharedObjects} from '../elements/shared/gr-rest-api-interface/gr-rest-api-interface.js';
-import {TestKeyboardShortcutBinder} from './test-utils';
+import {cleanupTestUtils, TestKeyboardShortcutBinder} from './test-utils.js';
 import {flushDebouncers} from '@polymer/polymer/lib/utils/debounce';
 import {_testOnly_getShortcutManagerInstance} from '../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
 import sinon from 'sinon/pkg/sinon-esm.js';
@@ -136,6 +136,7 @@
 
 teardown(() => {
   sinon.restore();
+  cleanupTestUtils();
   cleanups.forEach(cleanup => cleanup());
   cleanups.splice(0);
   TestKeyboardShortcutBinder.pop();
diff --git a/polygerrit-ui/app/test/test-utils.js b/polygerrit-ui/app/test/test-utils.js
index aaab295..5be9850 100644
--- a/polygerrit-ui/app/test/test-utils.js
+++ b/polygerrit-ui/app/test/test-utils.js
@@ -69,3 +69,19 @@
   const pl = _testOnly_resetPluginLoader();
   pl.loadPlugins([]);
 };
+
+const cleanups = [];
+
+function registerTestCleanup(cleanupCallback) {
+  cleanups.push(cleanupCallback);
+}
+
+export function cleanupTestUtils() {
+  cleanups.forEach(cleanup => cleanup());
+}
+
+export function stubBaseUrl(newUrl) {
+  const originalCanonicalPath = window.CANONICAL_PATH;
+  window.CANONICAL_PATH = newUrl;
+  registerTestCleanup(() => window.CANONICAL_PATH = originalCanonicalPath);
+}
diff --git a/polygerrit-ui/app/utils/async-util.js b/polygerrit-ui/app/utils/async-util.js
new file mode 100644
index 0000000..14d0288
--- /dev/null
+++ b/polygerrit-ui/app/utils/async-util.js
@@ -0,0 +1,38 @@
+/**
+ * @license
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @template T
+ * @param {!Array<T>} array
+ * @param {!Function} fn An iteratee function to be passed each element of
+ *     the array in order. Must return a promise, and the following
+ *     iteration will not begin until resolution of the promise returned by
+ *     the previous iteration.
+ *
+ *     An optional second argument to fn is a callback that will halt the
+ *     loop if called.
+ * @return {!Promise<undefined>}
+ */
+export function asyncForeach(array, fn) {
+  if (!array.length) { return Promise.resolve(); }
+  let stop = false;
+  const stopCallback = () => { stop = true; };
+  return fn(array[0], stopCallback).then(exit => {
+    if (stop) { return Promise.resolve(); }
+    return asyncForeach(array.slice(1), fn);
+  });
+}
diff --git a/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior_test.js b/polygerrit-ui/app/utils/async-util_test.js
similarity index 81%
rename from polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior_test.js
rename to polygerrit-ui/app/utils/async-util_test.js
index 0a44884..df29e97 100644
--- a/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior_test.js
+++ b/polygerrit-ui/app/utils/async-util_test.js
@@ -15,12 +15,13 @@
  * limitations under the License.
  */
 
-import '../../test/common-test-setup-karma.js';
-import {AsyncForeachBehavior} from './async-foreach-behavior.js';
-suite('async-foreach-behavior tests', () => {
+import '../test/common-test-setup-karma.js';
+import {asyncForeach} from './async-util.js';
+
+suite('async-util tests', () => {
   test('loops over each item', () => {
     const fn = sinon.stub().returns(Promise.resolve());
-    return AsyncForeachBehavior.asyncForeach([1, 2, 3], fn)
+    return asyncForeach([1, 2, 3], fn)
         .then(() => {
           assert.isTrue(fn.calledThrice);
           assert.equal(fn.getCall(0).args[0], 1);
@@ -36,7 +37,7 @@
       stop();
       return Promise.resolve();
     };
-    return AsyncForeachBehavior.asyncForeach([1, 2, 3], fn)
+    return asyncForeach([1, 2, 3], fn)
         .then(() => {
           assert.isTrue(stub.calledOnce);
           assert.equal(stub.lastCall.args[0], 1);
diff --git a/polygerrit-ui/app/utils/url-util.ts b/polygerrit-ui/app/utils/url-util.ts
new file mode 100644
index 0000000..a823bb4
--- /dev/null
+++ b/polygerrit-ui/app/utils/url-util.ts
@@ -0,0 +1,72 @@
+/**
+ * @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.
+ */
+const PROBE_PATH = '/Documentation/index.html';
+const DOCS_BASE_PATH = '/Documentation';
+
+// NOTE: Below we define 2 types (DocUrlBehaviorConfig and RestApi) to avoid
+// type 'any'. These are temporary definitions and they must be
+// updated/moved/removed when we start converting our codebase to typescript.
+// Right now we are using these types here just for adding typescript support to
+// our build/test infrastructure. Doing so we avoid massive code updates at this
+// stage.
+
+// TODO: introduce global gerrit config type instead of DocUrlBehaviorConfig.
+// The DocUrlBehaviorConfig is a temporary type
+interface DocUrlBehaviorConfig {
+  gerrit?: {doc_url?: string};
+}
+
+// TODO: implement RestApi type correctly and remove interface from this file
+interface RestApi {
+  probePath(url: string): Promise<boolean>;
+}
+
+export function getBaseUrl(): string {
+  return window.CANONICAL_PATH || '';
+}
+
+let getDocsBaseUrlCachedPromise: Promise<string | null> | undefined;
+
+/**
+ * Get the docs base URL from either the server config or by probing.
+ *
+ * @param {Object} config The server config.
+ * @param {!Object} restApi A REST API instance
+ * @return {!Promise<string>} A promise that resolves with the docs base
+ *     URL.
+ */
+export function getDocsBaseUrl(
+  config: DocUrlBehaviorConfig,
+  restApi: RestApi
+): Promise<string | null> {
+  if (!getDocsBaseUrlCachedPromise) {
+    getDocsBaseUrlCachedPromise = new Promise(resolve => {
+      if (config && config.gerrit && config.gerrit.doc_url) {
+        resolve(config.gerrit.doc_url);
+      } else {
+        restApi.probePath(getBaseUrl() + PROBE_PATH).then(ok => {
+          resolve(ok ? getBaseUrl() + DOCS_BASE_PATH : null);
+        });
+      }
+    });
+  }
+  return getDocsBaseUrlCachedPromise;
+}
+
+export function _testOnly_clearDocsBaseUrlCache() {
+  getDocsBaseUrlCachedPromise = undefined;
+}
diff --git a/polygerrit-ui/app/utils/url-util_test.js b/polygerrit-ui/app/utils/url-util_test.js
new file mode 100644
index 0000000..d3d3f2f
--- /dev/null
+++ b/polygerrit-ui/app/utils/url-util_test.js
@@ -0,0 +1,85 @@
+/**
+ * @license
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '../test/common-test-setup-karma.js';
+import {getBaseUrl, getDocsBaseUrl, _testOnly_clearDocsBaseUrlCache} from './url-util.js';
+
+suite('url-util tests', () => {
+  suite('getBaseUrl tests', () => {
+    let originialCanonicalPath;
+
+    suiteSetup(() => {
+      originialCanonicalPath = window.CANONICAL_PATH;
+      window.CANONICAL_PATH = '/r';
+    });
+
+    suiteTeardown(() => {
+      window.CANONICAL_PATH = originialCanonicalPath;
+    });
+
+    test('getBaseUrl', () => {
+      assert.deepEqual(getBaseUrl(), '/r');
+    });
+  });
+
+  suite('getDocsBaseUrl tests', () => {
+    setup(() => {
+      _testOnly_clearDocsBaseUrlCache();
+    });
+
+    test('null config', async () => {
+      const mockRestApi = {
+        probePath: sinon.stub().returns(Promise.resolve(true)),
+      };
+      const docsBaseUrl = await getDocsBaseUrl(null, mockRestApi);
+      assert.isTrue(
+          mockRestApi.probePath.calledWith('/Documentation/index.html'));
+      assert.equal(docsBaseUrl, '/Documentation');
+    });
+
+    test('no doc config', async () => {
+      const mockRestApi = {
+        probePath: sinon.stub().returns(Promise.resolve(true)),
+      };
+      const config = {gerrit: {}};
+      const docsBaseUrl = await getDocsBaseUrl(config, mockRestApi);
+      assert.isTrue(
+          mockRestApi.probePath.calledWith('/Documentation/index.html'));
+      assert.equal(docsBaseUrl, '/Documentation');
+    });
+
+    test('has doc config', async () => {
+      const mockRestApi = {
+        probePath: sinon.stub().returns(Promise.resolve(true)),
+      };
+      const config = {gerrit: {doc_url: 'foobar'}};
+      const docsBaseUrl = await getDocsBaseUrl(config, mockRestApi);
+      assert.isFalse(mockRestApi.probePath.called);
+      assert.equal(docsBaseUrl, 'foobar');
+    });
+
+    test('no probe', async () => {
+      const mockRestApi = {
+        probePath: sinon.stub().returns(Promise.resolve(false)),
+      };
+      const docsBaseUrl = await getDocsBaseUrl(null, mockRestApi);
+      assert.isTrue(
+          mockRestApi.probePath.calledWith('/Documentation/index.html'));
+      assert.isNotOk(docsBaseUrl);
+    });
+  });
+});