Merge changes I83d832d3,Ia6d33245

* changes:
  Add auto-saving for comments
  Completely rewrite commenting components and data model
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 f072fa3..338c015 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
@@ -20,10 +20,6 @@
 import './gr-change-metadata';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation';
 import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
-import {
-  _testOnly_initGerritPluginApi,
-  GerritInternal,
-} from '../../shared/gr-js-api-interface/gr-gerrit';
 import {GrChangeMetadata} from './gr-change-metadata';
 import {
   createServerInfo,
@@ -71,11 +67,9 @@
 const basicFixture = fixtureFromElement('gr-change-metadata');
 
 suite('gr-change-metadata tests', () => {
-  let pluginApi: GerritInternal;
   let element: GrChangeMetadata;
 
   setup(() => {
-    pluginApi = _testOnly_initGerritPluginApi();
     stubRestApi('getLoggedIn').returns(Promise.resolve(false));
     stubRestApi('getConfig').returns(
       Promise.resolve({
@@ -897,7 +891,7 @@
       }
       let hookEl: MetadataGrEndpointDecorator;
       let plugin: PluginApi;
-      pluginApi.install(
+      window.Gerrit.install(
         p => {
           plugin = p;
           plugin
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
index 734df6e..96e1796 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
@@ -32,10 +32,6 @@
 import {_testOnly_resetEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation';
 import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
-import {
-  _testOnly_initGerritPluginApi,
-  GerritInternal,
-} from '../../shared/gr-js-api-interface/gr-gerrit';
 import {EventType, PluginApi} from '../../../api/plugin';
 
 import 'lodash/lodash';
@@ -119,7 +115,6 @@
 
 suite('gr-change-view tests', () => {
   let element: GrChangeView;
-  let pluginApi: GerritInternal;
 
   let navigateToChangeStub: SinonStubbedMember<
     typeof GerritNav.navigateToChange
@@ -345,7 +340,6 @@
   ];
 
   setup(() => {
-    pluginApi = _testOnly_initGerritPluginApi();
     // Since pluginEndpoints are global, must reset state.
     _testOnly_resetEndpoints();
     navigateToChangeStub = sinon.stub(GerritNav, 'navigateToChange');
@@ -369,7 +363,7 @@
     element._changeNum = TEST_NUMERIC_CHANGE_ID;
     sinon.stub(element.$.actions, 'reload').returns(Promise.resolve());
     getPluginLoader().loadPlugins([]);
-    pluginApi.install(
+    window.Gerrit.install(
       plugin => {
         plugin.registerDynamicCustomComponent(
           'change-view-tab-header',
@@ -2143,7 +2137,11 @@
       element._change = {...createChangeViewChange(), labels: {}};
       element._selectedRevision = createRevision();
       const promise = mockPromise();
-      pluginApi.install(promise.resolve, '0.1', 'http://some/plugins/url.js');
+      window.Gerrit.install(
+        promise.resolve,
+        '0.1',
+        'http://some/plugins/url.js'
+      );
       await flush();
       const plugin: PluginApi = (await promise) as PluginApi;
       const hookEl = await plugin
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.ts b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.ts
index 68d48b7..94b8668 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.ts
@@ -47,10 +47,6 @@
 } from '../../../types/common';
 import {ParsedChangeInfo} from '../../../types/types';
 import {GrEndpointDecorator} from '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
-import {
-  _testOnly_initGerritPluginApi,
-  GerritInternal,
-} from '../../shared/gr-js-api-interface/gr-gerrit';
 import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
 import './gr-related-changes-list';
 import {
@@ -64,10 +60,8 @@
 
 suite('gr-related-changes-list', () => {
   let element: GrRelatedChangesList;
-  let pluginApi: GerritInternal;
 
   setup(() => {
-    pluginApi = _testOnly_initGerritPluginApi();
     element = basicFixture.instantiate();
   });
 
@@ -609,7 +603,7 @@
       }
       let hookEl: RelatedChangesListGrEndpointDecorator;
       let plugin: PluginApi;
-      pluginApi.install(
+      window.Gerrit.install(
         p => {
           plugin = p;
           plugin
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.js
index 8980642..8e78d4e 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.js
@@ -19,14 +19,12 @@
 import './gr-reply-dialog.js';
 
 import {queryAndAssert, resetPlugins, stubRestApi} from '../../../test/test-utils.js';
-import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
 import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
 
 const basicFixture = fixtureFromElement('gr-reply-dialog');
 
 suite('gr-reply-dialog-it tests', () => {
   let element;
-  let pluginApi;
   let changeNum;
   let patchNum;
 
@@ -71,7 +69,6 @@
   };
 
   setup(() => {
-    pluginApi = _testOnly_initGerritPluginApi();
     changeNum = 42;
     patchNum = 1;
 
@@ -102,7 +99,7 @@
 
   test('lgtm plugin', async () => {
     resetPlugins();
-    pluginApi.install(plugin => {
+    window.Gerrit.install(plugin => {
       const replyApi = plugin.changeReply();
       replyApi.addReplyTextChangedCallback(text => {
         const label = 'Code-Review';
diff --git a/polygerrit-ui/app/elements/gr-app-global-var-init.ts b/polygerrit-ui/app/elements/gr-app-global-var-init.ts
index de749df..d0525ea 100644
--- a/polygerrit-ui/app/elements/gr-app-global-var-init.ts
+++ b/polygerrit-ui/app/elements/gr-app-global-var-init.ts
@@ -26,10 +26,11 @@
 import {page} from '../utils/page-wrapper-utils';
 import {GrPluginActionContext} from './shared/gr-js-api-interface/gr-plugin-action-context';
 import {initGerritPluginApi} from './shared/gr-js-api-interface/gr-gerrit';
+import {AppContext} from '../services/app-context';
 
-export function initGlobalVariables() {
+export function initGlobalVariables(appContext: AppContext) {
   window.GrAnnotation = GrAnnotation;
   window.page = page;
   window.GrPluginActionContext = GrPluginActionContext;
-  initGerritPluginApi();
+  initGerritPluginApi(appContext);
 }
diff --git a/polygerrit-ui/app/elements/gr-app-init.ts b/polygerrit-ui/app/elements/gr-app-init.ts
deleted file mode 100644
index c63df04..0000000
--- a/polygerrit-ui/app/elements/gr-app-init.ts
+++ /dev/null
@@ -1,30 +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 {createAppContext} from '../services/app-context-init';
-import {
-  initVisibilityReporter,
-  initPerformanceReporter,
-  initErrorReporter,
-} from '../services/gr-reporting/gr-reporting_impl';
-import {injectAppContext} from '../services/app-context';
-
-const appContext = createAppContext();
-injectAppContext(appContext);
-const reportingService = appContext.reportingService;
-initVisibilityReporter(reportingService);
-initPerformanceReporter(reportingService);
-initErrorReporter(reportingService);
diff --git a/polygerrit-ui/app/elements/gr-app.ts b/polygerrit-ui/app/elements/gr-app.ts
index 463fab9..a8da03a 100644
--- a/polygerrit-ui/app/elements/gr-app.ts
+++ b/polygerrit-ui/app/elements/gr-app.ts
@@ -16,7 +16,6 @@
  */
 
 import {safeTypesBridge} from '../utils/safe-types-util';
-import './gr-app-init';
 import './font-roboto-local-loader';
 // Sets up global Polymer variable, because plugins requires it.
 import '../scripts/bundled-polymer';
@@ -38,10 +37,24 @@
 import './gr-app-element';
 import {PolymerElement} from '@polymer/polymer/polymer-element';
 import {htmlTemplate} from './gr-app_html';
-import {initGerritPluginApi} from './shared/gr-js-api-interface/gr-gerrit';
 import {customElement} from '@polymer/decorators';
 import {installPolymerResin} from '../scripts/polymer-resin-install';
 
+import {createAppContext} from '../services/app-context-init';
+import {
+  initVisibilityReporter,
+  initPerformanceReporter,
+  initErrorReporter,
+} from '../services/gr-reporting/gr-reporting_impl';
+import {injectAppContext} from '../services/app-context';
+
+const appContext = createAppContext();
+injectAppContext(appContext);
+const reportingService = appContext.reportingService;
+initVisibilityReporter(reportingService);
+initPerformanceReporter(reportingService);
+initErrorReporter(reportingService);
+
 installPolymerResin(safeTypesBridge);
 
 @customElement('gr-app')
@@ -57,5 +70,4 @@
   }
 }
 
-initGlobalVariables();
-initGerritPluginApi();
+initGlobalVariables(appContext);
diff --git a/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.js b/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.js
index 9a8f75e..6fd2505 100644
--- a/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.js
+++ b/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.js
@@ -18,16 +18,13 @@
 import '../../../test/common-test-setup-karma.js';
 import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
 import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
-import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
-
-const pluginApi = _testOnly_initGerritPluginApi();
 
 suite('gr-admin-api tests', () => {
   let adminApi;
 
   setup(() => {
     let plugin;
-    pluginApi.install(p => { plugin = p; }, '0.1',
+    window.Gerrit.install(p => { plugin = p; }, '0.1',
         'http://test.com/plugins/testplugin/static/test.js');
     getPluginLoader().loadPlugins([]);
     adminApi = plugin.admin();
diff --git a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.js b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.js
index 2d83012..94eb292 100644
--- a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.js
+++ b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.js
@@ -17,7 +17,6 @@
 
 import '../../../test/common-test-setup-karma.js';
 import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
-import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
 
 Polymer({
   is: 'gr-attribute-helper-some-element',
@@ -31,15 +30,13 @@
 
 const basicFixture = fixtureFromElement('gr-attribute-helper-some-element');
 
-const pluginApi = _testOnly_initGerritPluginApi();
-
 suite('gr-attribute-helper tests', () => {
   let element;
   let instance;
 
   setup(() => {
     let plugin;
-    pluginApi.install(p => { plugin = p; }, '0.1',
+    window.Gerrit.install(p => { plugin = p; }, '0.1',
         'http://test.com/plugins/testplugin/static/test.js');
     element = basicFixture.instantiate();
     instance = plugin.attributeHelper(element);
diff --git a/polygerrit-ui/app/elements/plugins/gr-checks-api/gr-checks-api_test.ts b/polygerrit-ui/app/elements/plugins/gr-checks-api/gr-checks-api_test.ts
index e1ec158..596c54b 100644
--- a/polygerrit-ui/app/elements/plugins/gr-checks-api/gr-checks-api_test.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-checks-api/gr-checks-api_test.ts
@@ -17,18 +17,15 @@
 
 import '../../../test/common-test-setup-karma';
 import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
-import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit';
 import {PluginApi} from '../../../api/plugin';
 import {ChecksPluginApi} from '../../../api/checks';
 
-const gerritPluginApi = _testOnly_initGerritPluginApi();
-
 suite('gr-settings-api tests', () => {
   let checksApi: ChecksPluginApi | undefined;
 
   setup(() => {
     let pluginApi: PluginApi | undefined = undefined;
-    gerritPluginApi.install(
+    window.Gerrit.install(
       p => {
         pluginApi = p;
       },
diff --git a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.js b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.js
index 883f2a6..025f2b4 100644
--- a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.js
+++ b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.js
@@ -18,9 +18,6 @@
 import '../../../test/common-test-setup-karma.js';
 import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
 import {GrDomHook, GrDomHooksManager} from './gr-dom-hooks.js';
-import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
-
-const pluginApi = _testOnly_initGerritPluginApi();
 
 suite('gr-dom-hooks tests', () => {
   let instance;
@@ -28,7 +25,7 @@
 
   setup(() => {
     let plugin;
-    pluginApi.install(p => { plugin = p; }, '0.1',
+    window.Gerrit.install(p => { plugin = p; }, '0.1',
         'http://test.com/plugins/testplugin/static/test.js');
     instance = new GrDomHooksManager(plugin);
   });
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.js b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.js
index 1be5e82..893f0d1 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.js
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.js
@@ -21,9 +21,6 @@
 import {html} from '@polymer/polymer/lib/utils/html-tag.js';
 import {resetPlugins} from '../../../test/test-utils.js';
 import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
-import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
-
-const pluginApi = _testOnly_initGerritPluginApi();
 
 const basicFixture = fixtureFromTemplate(
     html`<div>
@@ -54,7 +51,9 @@
   setup(async () => {
     resetPlugins();
     container = basicFixture.instantiate();
-    pluginApi.install(p => plugin = p, '0.1',
+    window.Gerrit.install(
+        p => { plugin = p; },
+        '0.1',
         'http://some/plugin/url.js');
     // Decoration
     decorationHook = plugin.registerCustomComponent('first', 'some-module');
diff --git a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.js b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.js
index 4e3d657..13bd535 100644
--- a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.js
+++ b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.js
@@ -18,7 +18,6 @@
 import '../../../test/common-test-setup-karma.js';
 import {addListener} from '@polymer/polymer/lib/utils/gestures.js';
 import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
-import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
 import {mockPromise} from '../../../test/test-utils.js';
 
 Polymer({
@@ -34,15 +33,13 @@
 
 const basicFixture = fixtureFromElement('gr-event-helper-some-element');
 
-const pluginApi = _testOnly_initGerritPluginApi();
-
 suite('gr-event-helper tests', () => {
   let element;
   let instance;
 
   setup(() => {
     let plugin;
-    pluginApi.install(p => { plugin = p; }, '0.1',
+    window.Gerrit.install(p => { plugin = p; }, '0.1',
         'http://test.com/plugins/testplugin/static/test.js');
     element = basicFixture.instantiate();
     instance = plugin.eventHelper(element);
diff --git a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.js b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.js
index a192f80..faf7525 100644
--- a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.js
+++ b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.js
@@ -18,11 +18,8 @@
 import {resetPlugins} from '../../../test/test-utils.js';
 import './gr-external-style.js';
 import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
-import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
 import {html} from '@polymer/polymer/lib/utils/html-tag.js';
 
-const pluginApi = _testOnly_initGerritPluginApi();
-
 const basicFixture = fixtureFromTemplate(
     html`<gr-external-style name="foo"></gr-external-style>`
 );
@@ -35,7 +32,7 @@
 
   const installPlugin = () => {
     if (plugin) { return; }
-    pluginApi.install(p => {
+    window.Gerrit.install(p => {
       plugin = p;
     }, '0.1', TEST_URL);
   };
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.js b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.js
index 2889333..beedfab 100644
--- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.js
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.js
@@ -18,7 +18,6 @@
 import '../../../test/common-test-setup-karma.js';
 import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
 import {GrPopupInterface} from './gr-popup-interface.js';
-import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
 import {html} from '@polymer/polymer/lib/utils/html-tag.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 
@@ -34,14 +33,13 @@
 
 const containerFixture = fixtureFromElement('div');
 
-const pluginApi = _testOnly_initGerritPluginApi();
 suite('gr-popup-interface tests', () => {
   let container;
   let instance;
   let plugin;
 
   setup(() => {
-    pluginApi.install(p => { plugin = p; }, '0.1',
+    window.Gerrit.install(p => { plugin = p; }, '0.1',
         'http://test.com/plugins/testplugin/static/test.js');
     container = containerFixture.instantiate();
     sinon.stub(plugin, 'hook').returns({
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api_test.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api_test.js
index 996edf3..1088b27 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api_test.js
@@ -17,9 +17,6 @@
 
 import '../../../test/common-test-setup-karma.js';
 import '../../change/gr-change-actions/gr-change-actions.js';
-import {_testOnly_initGerritPluginApi} from './gr-gerrit.js';
-
-const pluginApi = _testOnly_initGerritPluginApi();
 
 suite('gr-annotation-actions-js-api tests', () => {
   let annotationActions;
@@ -27,7 +24,7 @@
   let plugin;
 
   setup(() => {
-    pluginApi.install(p => { plugin = p; }, '0.1',
+    window.Gerrit.install(p => { plugin = p; }, '0.1',
         'http://test.com/plugins/testplugin/static/test.js');
     annotationActions = plugin.annotationApi();
   });
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.js
index 87f6052..b70c8ca 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.js
@@ -19,12 +19,8 @@
 import '../../change/gr-change-actions/gr-change-actions.js';
 import {resetPlugins} from '../../../test/test-utils.js';
 import {getPluginLoader} from './gr-plugin-loader.js';
-import {_testOnly_initGerritPluginApi} from './gr-gerrit.js';
-
 const basicFixture = fixtureFromElement('gr-change-actions');
 
-const pluginApi = _testOnly_initGerritPluginApi();
-
 suite('gr-change-actions-js-api-interface tests', () => {
   let element;
   let changeActions;
@@ -41,7 +37,7 @@
   suite('early init', () => {
     setup(() => {
       resetPlugins();
-      pluginApi.install(p => { plugin = p; }, '0.1',
+      window.Gerrit.install(p => { plugin = p; }, '0.1',
           'http://test.com/plugins/testplugin/static/test.js');
       // Mimic all plugins loaded.
       getPluginLoader().loadPlugins([]);
@@ -68,7 +64,7 @@
       sinon.stub(element, '_editStatusChanged');
       element.change = {};
       element._hasKnownChainState = false;
-      pluginApi.install(p => { plugin = p; }, '0.1',
+      window.Gerrit.install(p => { plugin = p; }, '0.1',
           'http://test.com/plugins/testplugin/static/test.js');
       changeActions = plugin.changeActions();
       // Mimic all plugins loaded.
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.js
index 2324588..52d6ab3 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.js
@@ -17,16 +17,12 @@
 
 import '../../../test/common-test-setup-karma.js';
 import '../../change/gr-reply-dialog/gr-reply-dialog.js';
-import {_testOnly_initGerritPluginApi} from './gr-gerrit.js';
 import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-reply-dialog');
 
-const pluginApi = _testOnly_initGerritPluginApi();
-
 suite('gr-change-reply-js-api tests', () => {
   let element;
-
   let changeReply;
   let plugin;
 
@@ -36,7 +32,7 @@
 
   suite('early init', () => {
     setup(() => {
-      pluginApi.install(p => { plugin = p; }, '0.1',
+      window.Gerrit.install(p => { plugin = p; }, '0.1',
           'http://test.com/plugins/testplugin/static/test.js');
       changeReply = plugin.changeReply();
       element = basicFixture.instantiate();
@@ -64,7 +60,7 @@
   suite('normal init', () => {
     setup(() => {
       element = basicFixture.instantiate();
-      pluginApi.install(p => { plugin = p; }, '0.1',
+      window.Gerrit.install(p => { plugin = p; }, '0.1',
           'http://test.com/plugins/testplugin/static/test.js');
       changeReply = plugin.changeReply();
     });
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.ts
index d76b2b7..07fad80 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.ts
@@ -81,12 +81,12 @@
   Auth: AuthService;
 }
 
-export function initGerritPluginApi() {
-  window.Gerrit = window.Gerrit ?? new GerritImpl(getAppContext());
+export function initGerritPluginApi(appContext: AppContext) {
+  window.Gerrit = window.Gerrit ?? new GerritImpl(appContext);
 }
 
-export function _testOnly_initGerritPluginApi(): GerritInternal {
-  initGerritPluginApi();
+export function _testOnly_getGerritInternalPluginApi(): GerritInternal {
+  if (!window.Gerrit) throw new Error('initGerritPluginApi was not called');
   return window.Gerrit as GerritInternal;
 }
 
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit_test.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit_test.js
index ae0c370..d53c266 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit_test.js
@@ -18,7 +18,7 @@
 import '../../../test/common-test-setup-karma.js';
 import {getPluginLoader} from './gr-plugin-loader.js';
 import {resetPlugins} from '../../../test/test-utils.js';
-import {_testOnly_initGerritPluginApi} from './gr-gerrit.js';
+import {_testOnly_getGerritInternalPluginApi} from './gr-gerrit.js';
 import {stubRestApi} from '../../../test/test-utils.js';
 import {getAppContext} from '../../../services/app-context.js';
 
@@ -33,7 +33,7 @@
     stubRestApi('getAccount').returns(Promise.resolve({name: 'Judy Hopps'}));
     stubRestApi('send').returns(Promise.resolve({status: 200}));
     element = getAppContext().jsApiService;
-    pluginApi = _testOnly_initGerritPluginApi();
+    pluginApi = _testOnly_getGerritInternalPluginApi();
   });
 
   teardown(() => {
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 a48f91c..c45bbf5 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
@@ -21,13 +21,10 @@
 import {EventType} from '../../../api/plugin.js';
 import {PLUGIN_LOADING_TIMEOUT_MS} from './gr-api-utils.js';
 import {getPluginLoader} from './gr-plugin-loader.js';
-import {_testOnly_initGerritPluginApi} from './gr-gerrit.js';
 import {stubBaseUrl} from '../../../test/test-utils.js';
 import {stubRestApi} from '../../../test/test-utils.js';
 import {getAppContext} from '../../../services/app-context.js';
 
-const pluginApi = _testOnly_initGerritPluginApi();
-
 suite('GrJsApiInterface tests', () => {
   let element;
   let plugin;
@@ -47,7 +44,7 @@
     sendStub = stubRestApi('send').returns(Promise.resolve({status: 200}));
     element = getAppContext().jsApiService;
     errorStub = sinon.stub(element.reporting, 'error');
-    pluginApi.install(p => { plugin = p; }, '0.1',
+    window.Gerrit.install(p => { plugin = p; }, '0.1',
         'http://test.com/plugins/testplugin/static/test.js');
     getPluginLoader().loadPlugins([]);
   });
@@ -300,7 +297,7 @@
     setup(() => {
       stubBaseUrl('/r');
 
-      pluginApi.install(p => { baseUrlPlugin = p; }, '0.1',
+      window.Gerrit.install(p => { baseUrlPlugin = p; }, '0.1',
           'http://test.com/r/plugins/baseurlplugin/static/test.js');
     });
 
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.js
index 34c976a..d4b93a7 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.js
@@ -18,18 +18,15 @@
 import '../../../test/common-test-setup-karma.js';
 import './gr-js-api-interface.js';
 import {GrPluginActionContext} from './gr-plugin-action-context.js';
-import {_testOnly_initGerritPluginApi} from './gr-gerrit.js';
 import {addListenerForTest} from '../../../test/test-utils.js';
 
-const pluginApi = _testOnly_initGerritPluginApi();
-
 suite('gr-plugin-action-context tests', () => {
   let instance;
 
   let plugin;
 
   setup(() => {
-    pluginApi.install(p => { plugin = p; }, '0.1',
+    window.Gerrit.install(p => { plugin = p; }, '0.1',
         'http://test.com/plugins/testplugin/static/test.js');
     instance = new GrPluginActionContext(plugin);
   });
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.ts
index c7bdfb4..16846f4 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.ts
@@ -18,12 +18,9 @@
 import {resetPlugins} from '../../../test/test-utils';
 import './gr-js-api-interface';
 import {GrPluginEndpoints} from './gr-plugin-endpoints';
-import {_testOnly_initGerritPluginApi} from './gr-gerrit';
 import {PluginApi} from '../../../api/plugin';
 import {HookApi, HookCallback, PluginElement} from '../../../api/hook';
 
-const pluginApi = _testOnly_initGerritPluginApi();
-
 export class MockHook<T extends PluginElement> implements HookApi<T> {
   handleInstanceDetached(_: T) {}
 
@@ -59,7 +56,7 @@
   setup(() => {
     domHook = new MockHook<PluginElement>();
     instance = new GrPluginEndpoints();
-    pluginApi.install(
+    window.Gerrit.install(
       plugin => (decoratePlugin = plugin),
       '0.1',
       'http://test.com/plugins/testplugin/static/decorate.js'
@@ -70,7 +67,7 @@
       moduleName: 'decorate-module',
       domHook,
     });
-    pluginApi.install(
+    window.Gerrit.install(
       plugin => (stylePlugin = plugin),
       '0.1',
       'http://test.com/plugins/testplugin/static/style.js'
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 ab69267..e097858 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
@@ -19,11 +19,8 @@
 import {PLUGIN_LOADING_TIMEOUT_MS} from './gr-api-utils.js';
 import {_testOnly_resetPluginLoader} from './gr-plugin-loader.js';
 import {resetPlugins, stubBaseUrl} from '../../../test/test-utils.js';
-import {_testOnly_initGerritPluginApi} from './gr-gerrit.js';
 import {addListenerForTest, stubRestApi} from '../../../test/test-utils.js';
 
-const pluginApi = _testOnly_initGerritPluginApi();
-
 suite('gr-plugin-loader tests', () => {
   let plugin;
 
@@ -47,18 +44,18 @@
   });
 
   test('reuse plugin for install calls', () => {
-    pluginApi.install(p => { plugin = p; }, '0.1',
+    window.Gerrit.install(p => { plugin = p; }, '0.1',
         'http://test.com/plugins/testplugin/static/test.js');
 
     let otherPlugin;
-    pluginApi.install(p => { otherPlugin = p; }, '0.1',
+    window.Gerrit.install(p => { otherPlugin = p; }, '0.1',
         'http://test.com/plugins/testplugin/static/test.js');
     assert.strictEqual(plugin, otherPlugin);
   });
 
   test('versioning', () => {
     const callback = sinon.spy();
-    pluginApi.install(callback, '0.0pre-alpha');
+    window.Gerrit.install(callback, '0.0pre-alpha');
     assert(callback.notCalled);
   });
 
@@ -89,7 +86,7 @@
 
   test('plugins installed successfully', async () => {
     sinon.stub(pluginLoader, '_loadJsPlugin').callsFake( url => {
-      pluginApi.install(() => void 0, undefined, url);
+      window.Gerrit.install(() => void 0, undefined, url);
     });
     const pluginsLoadedStub = sinon.stub(pluginLoader._getReporting(),
         'pluginsLoaded');
@@ -107,7 +104,7 @@
 
   test('isPluginEnabled and isPluginLoaded', () => {
     sinon.stub(pluginLoader, '_loadJsPlugin').callsFake( url => {
-      pluginApi.install(() => void 0, undefined, url);
+      window.Gerrit.install(() => void 0, undefined, url);
     });
 
     const plugins = [
@@ -137,7 +134,7 @@
     addListenerForTest(document, 'show-alert', alertStub);
 
     sinon.stub(pluginLoader, '_loadJsPlugin').callsFake( url => {
-      pluginApi.install(() => {
+      window.Gerrit.install(() => {
         if (url === plugins[0]) {
           throw new Error('failed');
         }
@@ -165,7 +162,7 @@
     addListenerForTest(document, 'show-alert', alertStub);
 
     sinon.stub(pluginLoader, '_loadJsPlugin').callsFake( url => {
-      pluginApi.install(() => {
+      window.Gerrit.install(() => {
         if (url === plugins[0]) {
           throw new Error('failed');
         }
@@ -198,7 +195,7 @@
     addListenerForTest(document, 'show-alert', alertStub);
 
     sinon.stub(pluginLoader, '_loadJsPlugin').callsFake( url => {
-      pluginApi.install(() => {
+      window.Gerrit.install(() => {
         throw new Error('failed');
       }, undefined, url);
     });
@@ -224,7 +221,7 @@
     addListenerForTest(document, 'show-alert', alertStub);
 
     sinon.stub(pluginLoader, '_loadJsPlugin').callsFake( url => {
-      pluginApi.install(() => {
+      window.Gerrit.install(() => {
       }, url === plugins[0] ? '' : 'alpha', url);
     });
 
@@ -241,7 +238,7 @@
 
   test('multiple assets for same plugin installed successfully', async () => {
     sinon.stub(pluginLoader, '_loadJsPlugin').callsFake( url => {
-      pluginApi.install(() => void 0, undefined, url);
+      window.Gerrit.install(() => void 0, undefined, url);
     });
     const pluginsLoadedStub = sinon.stub(pluginLoader._getReporting(),
         'pluginsLoaded');
@@ -388,7 +385,7 @@
       }
     }
     sinon.stub(pluginLoader, '_loadJsPlugin').callsFake( url => {
-      pluginApi.install(() => pluginCallback(url), undefined, url);
+      window.Gerrit.install(() => pluginCallback(url), undefined, url);
     });
 
     pluginLoader.loadPlugins(plugins);
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.js
index d2b5658..730f163 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.js
@@ -18,11 +18,8 @@
 import '../../../test/common-test-setup-karma.js';
 import './gr-js-api-interface.js';
 import {GrPluginRestApi} from './gr-plugin-rest-api.js';
-import {_testOnly_initGerritPluginApi} from './gr-gerrit.js';
 import {stubRestApi} from '../../../test/test-utils.js';
 
-const pluginApi = _testOnly_initGerritPluginApi();
-
 suite('gr-plugin-rest-api tests', () => {
   let instance;
   let getResponseObjectStub;
@@ -33,7 +30,7 @@
     getResponseObjectStub = stubRestApi('getResponseObject').returns(
         Promise.resolve());
     sendStub = stubRestApi('send').returns(Promise.resolve({status: 200}));
-    pluginApi.install(p => {}, '0.1',
+    window.Gerrit.install(p => {}, '0.1',
         'http://test.com/plugins/testplugin/static/test.js');
     instance = new GrPluginRestApi();
   });
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-reporting-js-api_test.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-reporting-js-api_test.ts
index a0f2e02..c96a075 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-reporting-js-api_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-reporting-js-api_test.ts
@@ -17,7 +17,6 @@
 
 import '../../../test/common-test-setup-karma.js';
 import '../../change/gr-reply-dialog/gr-reply-dialog.js';
-import {GerritInternal, _testOnly_initGerritPluginApi} from './gr-gerrit.js';
 import {getAppContext} from '../../../services/app-context.js';
 import {stubRestApi} from '../../../test/test-utils.js';
 import {PluginApi} from '../../../api/plugin.js';
@@ -27,10 +26,8 @@
 suite('gr-reporting-js-api tests', () => {
   let plugin: PluginApi;
   let reportingService: ReportingService;
-  let pluginApi: GerritInternal;
 
   setup(() => {
-    pluginApi = _testOnly_initGerritPluginApi();
     stubRestApi('getAccount').returns(Promise.resolve(undefined));
     reportingService = getAppContext().reportingService;
   });
@@ -38,7 +35,7 @@
   suite('early init', () => {
     let reporting: ReportingPluginApi;
     setup(() => {
-      pluginApi.install(
+      window.Gerrit.install(
         p => {
           plugin = p;
         },
diff --git a/polygerrit-ui/app/embed/gr-diff-app-context-init.ts b/polygerrit-ui/app/embed/gr-diff-app-context-init.ts
index 16308c0..44db793 100644
--- a/polygerrit-ui/app/embed/gr-diff-app-context-init.ts
+++ b/polygerrit-ui/app/embed/gr-diff-app-context-init.ts
@@ -16,7 +16,7 @@
  */
 
 import {create, Registry, Finalizable} from '../services/registry';
-import {AppContext, injectAppContext} from '../services/app-context';
+import {AppContext} from '../services/app-context';
 import {AuthService} from '../services/gr-auth/gr-auth';
 import {FlagsService} from '../services/flags/flags';
 import {grReportingMock} from '../services/gr-reporting/gr-reporting_mock';
@@ -61,12 +61,10 @@
   }
 }
 
-let appContext: (AppContext & Finalizable) | undefined;
-
 // Setup mocks for appContext.
 // This is a temporary solution
 // TODO(dmfilippov): find a better solution for gr-diff
-export function initDiffAppContext() {
+export function createDiffAppContext(): AppContext & Finalizable {
   const appRegistry: Registry<AppContext> = {
     flagsService: (_ctx: Partial<AppContext>) => new MockFlagsService(),
     authService: (_ctx: Partial<AppContext>) => new MockAuthService(),
@@ -105,6 +103,5 @@
       throw new Error('browserModel is not implemented');
     },
   };
-  appContext = create<AppContext>(appRegistry);
-  injectAppContext(appContext);
+  return create<AppContext>(appRegistry);
 }
diff --git a/polygerrit-ui/app/embed/gr-diff-app-context-init_test.js b/polygerrit-ui/app/embed/gr-diff-app-context-init_test.js
index 7f964f4..bb46484 100644
--- a/polygerrit-ui/app/embed/gr-diff-app-context-init_test.js
+++ b/polygerrit-ui/app/embed/gr-diff-app-context-init_test.js
@@ -16,18 +16,11 @@
  */
 
 import '../test/common-test-setup-karma.js';
-import {getAppContext} from '../services/app-context.js';
-import {
-  initDiffAppContext,
-} from './gr-diff-app-context-init.js';
+import {createDiffAppContext} from './gr-diff-app-context-init.js';
 
 suite('gr diff app context initializer tests', () => {
-  setup(() => {
-    initDiffAppContext();
-  });
-
   test('all services initialized and are singletons', () => {
-    const appContext = getAppContext();
+    const appContext = createDiffAppContext();
     Object.keys(appContext).forEach(serviceName => {
       const service = appContext[serviceName];
       assert.isNotNull(service);
diff --git a/polygerrit-ui/app/embed/gr-diff.ts b/polygerrit-ui/app/embed/gr-diff.ts
index 422667a4..64ef214 100644
--- a/polygerrit-ui/app/embed/gr-diff.ts
+++ b/polygerrit-ui/app/embed/gr-diff.ts
@@ -28,11 +28,12 @@
 import {TokenHighlightLayer} from '../elements/diff/gr-diff-builder/token-highlight-layer';
 import {GrDiffCursor} from '../elements/diff/gr-diff-cursor/gr-diff-cursor';
 import {GrAnnotation} from '../elements/diff/gr-diff-highlight/gr-annotation';
-import {initDiffAppContext} from './gr-diff-app-context-init';
+import {createDiffAppContext} from './gr-diff-app-context-init';
+import {injectAppContext} from '../services/app-context';
 
 // Setup appContext for diff.
 // TODO (dmfilippov): find a better solution
-initDiffAppContext();
+injectAppContext(createDiffAppContext());
 // Setup global variables for existing usages of this component
 window.grdiff = {
   GrAnnotation,
diff --git a/polygerrit-ui/app/services/change/change-model.ts b/polygerrit-ui/app/services/change/change-model.ts
index 458f610..2c98320 100644
--- a/polygerrit-ui/app/services/change/change-model.ts
+++ b/polygerrit-ui/app/services/change/change-model.ts
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-import {PatchSetNum} from '../../types/common';
+import {NumericChangeId, PatchSetNum} from '../../types/common';
 import {BehaviorSubject, combineLatest, Observable} from 'rxjs';
 import {
   map,
@@ -78,24 +78,8 @@
 // Re-exporting as Observable so that you can only subscribe, but not emit.
 export const changeState$: Observable<ChangeState> = privateState$;
 
-// Must only be used by the change service or whatever is in control of this
-// model.
 export function updateStateChange(change?: ParsedChangeInfo) {
   const current = privateState$.getValue();
-  // We want to make it easy for subscribers to react to change changes, so we
-  // are explicitly emitting an additional `undefined` when the change number
-  // changes. So if you are subscribed to the latestPatchsetNumber for example,
-  // then you can rely on emissions even if the old and the new change have the
-  // same latestPatchsetNumber.
-  if (change !== undefined && current.change !== undefined) {
-    if (change._number !== current.change._number) {
-      privateState$.next({
-        ...current,
-        change: undefined,
-        loadingStatus: LoadingStatus.NOT_LOADED,
-      });
-    }
-  }
   privateState$.next({
     ...current,
     change,
@@ -104,14 +88,22 @@
   });
 }
 
-export function updateStateLoading() {
+/**
+ * Called when change detail loading is initiated.
+ *
+ * If the change number matches the current change in the state, then
+ * this is a reload. If not, then we not just want to set the state to
+ * LOADING instead of RELOADING, but we also want to set the change to
+ * undefined right away. Otherwise components could see inconsistent state:
+ * a new change number, but an old change.
+ */
+export function updateStateLoading(changeNum: NumericChangeId) {
   const current = privateState$.getValue();
+  const reloading = current.change?._number === changeNum;
   privateState$.next({
     ...current,
-    loadingStatus:
-      current.change === undefined
-        ? LoadingStatus.LOADING
-        : LoadingStatus.RELOADING,
+    change: reloading ? current.change : undefined,
+    loadingStatus: reloading ? LoadingStatus.RELOADING : LoadingStatus.LOADING,
   });
 }
 
diff --git a/polygerrit-ui/app/services/change/change-service.ts b/polygerrit-ui/app/services/change/change-service.ts
index c1b8c9b..bcc62b2 100644
--- a/polygerrit-ui/app/services/change/change-service.ts
+++ b/polygerrit-ui/app/services/change/change-service.ts
@@ -50,7 +50,7 @@
         .pipe(
           map(([changeNum, _]) => changeNum),
           switchMap(changeNum => {
-            if (changeNum !== undefined) updateStateLoading();
+            if (changeNum !== undefined) updateStateLoading(changeNum);
             return from(this.restApiService.getChangeDetail(changeNum));
           })
         )
diff --git a/polygerrit-ui/app/services/change/change-services_test.ts b/polygerrit-ui/app/services/change/change-services_test.ts
index f8aeb46..d05df85a 100644
--- a/polygerrit-ui/app/services/change/change-services_test.ts
+++ b/polygerrit-ui/app/services/change/change-services_test.ts
@@ -15,6 +15,8 @@
  * limitations under the License.
  */
 
+import {Subject} from 'rxjs';
+import {takeUntil} from 'rxjs/operators';
 import {ChangeStatus} from '../../constants/constants';
 import '../../test/common-test-setup-karma';
 import {
@@ -23,7 +25,7 @@
   createRevision,
 } from '../../test/test-data-generators';
 import {mockPromise, stubRestApi, waitUntil} from '../../test/test-utils';
-import {CommitId, PatchSetNum} from '../../types/common';
+import {CommitId, NumericChangeId, PatchSetNum} from '../../types/common';
 import {ParsedChangeInfo} from '../../types/types';
 import {getAppContext} from '../app-context';
 import {
@@ -36,6 +38,7 @@
 suite('change service tests', () => {
   let changeService: ChangeService;
   let knownChange: ParsedChangeInfo;
+  const testCompleted = new Subject<void>();
   setup(() => {
     changeService = new ChangeService(getAppContext().restApiService);
     knownChange = {
@@ -60,15 +63,16 @@
 
   teardown(() => {
     changeService.finalize();
+    testCompleted.next();
   });
 
-  test('change not loaded, loading, reloading, ...', async () => {
-    let promise = mockPromise<ParsedChangeInfo | undefined>();
+  test('load a change', async () => {
+    const promise = mockPromise<ParsedChangeInfo | undefined>();
     const stub = stubRestApi('getChangeDetail').callsFake(() => promise);
     let state: ChangeState | undefined = {
       loadingStatus: LoadingStatus.NOT_LOADED,
     };
-    changeState$.subscribe(s => (state = s));
+    changeState$.pipe(takeUntil(testCompleted)).subscribe(s => (state = s));
 
     await waitUntil(() => state?.loadingStatus === LoadingStatus.NOT_LOADED);
     assert.equal(stub.callCount, 0);
@@ -83,8 +87,21 @@
     await waitUntil(() => state?.loadingStatus === LoadingStatus.LOADED);
     assert.equal(stub.callCount, 1);
     assert.equal(state?.change, knownChange);
+  });
 
-    promise = mockPromise<ParsedChangeInfo | undefined>();
+  test('reload a change', async () => {
+    // setting up a loaded change
+    const promise = mockPromise<ParsedChangeInfo | undefined>();
+    const stub = stubRestApi('getChangeDetail').callsFake(() => promise);
+    let state: ChangeState | undefined = {
+      loadingStatus: LoadingStatus.NOT_LOADED,
+    };
+    changeState$.pipe(takeUntil(testCompleted)).subscribe(s => (state = s));
+    setRouterState({view: GerritView.CHANGE, changeNum: knownChange._number});
+    promise.resolve(knownChange);
+    await waitUntil(() => state?.loadingStatus === LoadingStatus.LOADED);
+
+    // Reloading same change
     document.dispatchEvent(new CustomEvent('reload'));
     await waitUntil(() => state?.loadingStatus === LoadingStatus.RELOADING);
     assert.equal(stub.callCount, 2);
@@ -94,23 +111,66 @@
     await waitUntil(() => state?.loadingStatus === LoadingStatus.LOADED);
     assert.equal(stub.callCount, 2);
     assert.equal(state?.change, knownChange);
+  });
+
+  test('navigating to another change', async () => {
+    // setting up a loaded change
+    let promise = mockPromise<ParsedChangeInfo | undefined>();
+    const stub = stubRestApi('getChangeDetail').callsFake(() => promise);
+    let state: ChangeState | undefined = {
+      loadingStatus: LoadingStatus.NOT_LOADED,
+    };
+    changeState$.pipe(takeUntil(testCompleted)).subscribe(s => (state = s));
+    setRouterState({view: GerritView.CHANGE, changeNum: knownChange._number});
+    promise.resolve(knownChange);
+    await waitUntil(() => state?.loadingStatus === LoadingStatus.LOADED);
+
+    // Navigating to other change
+
+    const otherChange: ParsedChangeInfo = {
+      ...knownChange,
+      _number: 123 as NumericChangeId,
+    };
+    promise = mockPromise<ParsedChangeInfo | undefined>();
+    setRouterState({view: GerritView.CHANGE, changeNum: otherChange._number});
+    await waitUntil(() => state?.loadingStatus === LoadingStatus.LOADING);
+    assert.equal(stub.callCount, 2);
+    assert.isUndefined(state?.change);
+
+    promise.resolve(otherChange);
+    await waitUntil(() => state?.loadingStatus === LoadingStatus.LOADED);
+    assert.equal(stub.callCount, 2);
+    assert.equal(state?.change, otherChange);
+  });
+
+  test('navigating to dashboard', async () => {
+    // setting up a loaded change
+    let promise = mockPromise<ParsedChangeInfo | undefined>();
+    const stub = stubRestApi('getChangeDetail').callsFake(() => promise);
+    let state: ChangeState | undefined = {
+      loadingStatus: LoadingStatus.NOT_LOADED,
+    };
+    changeState$.pipe(takeUntil(testCompleted)).subscribe(s => (state = s));
+    setRouterState({view: GerritView.CHANGE, changeNum: knownChange._number});
+    promise.resolve(knownChange);
+    await waitUntil(() => state?.loadingStatus === LoadingStatus.LOADED);
+
+    // Navigating to dashboard
 
     promise = mockPromise<ParsedChangeInfo | undefined>();
     promise.resolve(undefined);
     setRouterState({view: GerritView.DASHBOARD, changeNum: undefined});
     await waitUntil(() => state?.loadingStatus === LoadingStatus.NOT_LOADED);
-    assert.equal(stub.callCount, 3);
+    assert.equal(stub.callCount, 2);
     assert.isUndefined(state?.change);
 
+    // Navigating back from dashboard to change page
+
     promise = mockPromise<ParsedChangeInfo | undefined>();
-    setRouterState({view: GerritView.CHANGE, changeNum: knownChange._number});
-    await waitUntil(() => state?.loadingStatus === LoadingStatus.LOADING);
-    assert.equal(stub.callCount, 4);
-    assert.isUndefined(state?.change);
-
     promise.resolve(knownChange);
+    setRouterState({view: GerritView.CHANGE, changeNum: knownChange._number});
     await waitUntil(() => state?.loadingStatus === LoadingStatus.LOADED);
-    assert.equal(stub.callCount, 4);
+    assert.equal(stub.callCount, 3);
     assert.equal(state?.change, knownChange);
   });
 
diff --git a/polygerrit-ui/app/test/common-test-setup.ts b/polygerrit-ui/app/test/common-test-setup.ts
index 92b61e7..fc621a2 100644
--- a/polygerrit-ui/app/test/common-test-setup.ts
+++ b/polygerrit-ui/app/test/common-test-setup.ts
@@ -21,10 +21,9 @@
 import '../scripts/bundled-polymer';
 import '@polymer/iron-test-helpers/iron-test-helpers';
 import './test-router';
-import {
-  _testOnlyInitAppContext,
-  _testOnlyFinalizeAppContext,
-} from './test-app-context-init';
+import {AppContext, injectAppContext} from '../services/app-context';
+import {Finalizable} from '../services/registry';
+import {createTestAppContext} from './test-app-context-init';
 import {_testOnly_resetPluginLoader} from '../elements/shared/gr-js-api-interface/gr-plugin-loader';
 import {_testOnlyResetGrRestApiSharedObjects} from '../elements/shared/gr-rest-api-interface/gr-rest-api-impl';
 import {
@@ -36,7 +35,6 @@
   removeThemeStyles,
 } from './test-utils';
 import {safeTypesBridge} from '../utils/safe-types-util';
-import {_testOnly_initGerritPluginApi} from '../elements/shared/gr-js-api-interface/gr-gerrit';
 import {initGlobalVariables} from '../elements/gr-app-global-var-init';
 import 'chai/chai';
 import {chaiDomDiff} from '@open-wc/semantic-dom-diff';
@@ -47,7 +45,6 @@
 import {_testOnly_allTasks} from '../utils/async-util';
 import {cleanUpStorage} from '../services/storage/gr-storage_mock';
 
-import {getAppContext} from '../services/app-context';
 import {_testOnly_resetState as resetChangeState} from '../services/change/change-model';
 import {_testOnly_resetState as resetCommentsState} from '../services/comments/comments-model';
 import {_testOnly_resetState as resetRouterState} from '../services/router/router-model';
@@ -101,6 +98,7 @@
 
 window.fixture = fixtureImpl;
 let testSetupTimestampMs = 0;
+let appContext: AppContext & Finalizable;
 
 setup(() => {
   testSetupTimestampMs = new Date().getTime();
@@ -109,17 +107,17 @@
   // If the following asserts fails - then window.stub is
   // overwritten by some other code.
   assert.equal(getCleanupsCount(), 0);
-  _testOnlyInitAppContext();
+  appContext = createTestAppContext();
+  injectAppContext(appContext);
   // The following calls is nessecary to avoid influence of previously executed
   // tests.
-  initGlobalVariables();
-  _testOnly_initGerritPluginApi();
+  initGlobalVariables(appContext);
 
   resetChangeState();
   resetCommentsState();
   resetRouterState();
 
-  const shortcuts = getAppContext().shortcutsService;
+  const shortcuts = appContext.shortcutsService;
   assert.isTrue(shortcuts._testOnly_isEmpty());
   const selection = document.getSelection();
   if (selection) {
@@ -215,7 +213,7 @@
   cancelAllTasks();
   cleanUpStorage();
   // Reset state
-  _testOnlyFinalizeAppContext();
+  appContext?.finalize();
   const testTeardownTimestampMs = new Date().getTime();
   const elapsedMs = testTeardownTimestampMs - testSetupTimestampMs;
   if (elapsedMs > 1000) {
diff --git a/polygerrit-ui/app/test/test-app-context-init.ts b/polygerrit-ui/app/test/test-app-context-init.ts
index 8bf6b25..946f813 100644
--- a/polygerrit-ui/app/test/test-app-context-init.ts
+++ b/polygerrit-ui/app/test/test-app-context-init.ts
@@ -18,7 +18,7 @@
 // Init app context before any other imports
 import {create, Registry, Finalizable} from '../services/registry';
 import {assertIsDefined} from '../utils/common-util';
-import {AppContext, injectAppContext} from '../services/app-context';
+import {AppContext} from '../services/app-context';
 import {grReportingMock} from '../services/gr-reporting/gr-reporting_mock';
 import {grRestApiMock} from './mocks/gr-rest-api_mock';
 import {grStorageMock} from '../services/storage/gr-storage_mock';
@@ -34,9 +34,7 @@
 import {BrowserModel} from '../services/browser/browser-model';
 import {ConfigModel} from '../services/config/config-model';
 
-let appContext: (AppContext & Finalizable) | undefined;
-
-export function _testOnlyInitAppContext() {
+export function createTestAppContext(): AppContext & Finalizable {
   const appRegistry: Registry<AppContext> = {
     flagsService: (_ctx: Partial<AppContext>) =>
       new FlagsServiceImplementation(),
@@ -83,11 +81,5 @@
       return new BrowserModel(ctx.userModel!);
     },
   };
-  appContext = create<AppContext>(appRegistry);
-  injectAppContext(appContext);
-}
-
-export function _testOnlyFinalizeAppContext() {
-  appContext?.finalize();
-  appContext = undefined;
+  return create<AppContext>(appRegistry);
 }
diff --git a/polygerrit-ui/app/test/test-utils.ts b/polygerrit-ui/app/test/test-utils.ts
index 1cb2727..50e08fb 100644
--- a/polygerrit-ui/app/test/test-utils.ts
+++ b/polygerrit-ui/app/test/test-utils.ts
@@ -192,13 +192,14 @@
   const start = Date.now();
   let sleep = 0;
   if (predicate()) return Promise.resolve();
+  const error = new Error(message);
   return new Promise((resolve, reject) => {
     const waiter = () => {
       if (predicate()) {
         return resolve();
       }
       if (Date.now() - start >= 1000) {
-        return reject(new Error(message));
+        return reject(error);
       }
       setTimeout(waiter, sleep);
       sleep = sleep === 0 ? 1 : sleep * 4;