Move to Dependency injection for UserModel

Make model getters private on elements. They can be easily
fetched with `testResolver(fooToken)` in tests.

In addition, get rid of shortcutsService on appContext as it was not
being used anywhere and shortcutsService is already DI.

Release-Notes: skip
Change-Id: I2f565127a3cdb7e79206500550e4313c23a7fb09
diff --git a/polygerrit-ui/FE_Style_Guide.md b/polygerrit-ui/FE_Style_Guide.md
index 6673cdf..56b5aee 100644
--- a/polygerrit-ui/FE_Style_Guide.md
+++ b/polygerrit-ui/FE_Style_Guide.md
@@ -167,83 +167,3 @@
 the element's class constructor.
 
 Do not use appContext anywhere except the constructor of the class.
-
-**Note for legacy elements:** If a polymer element extends a LegacyElementMixin and overrides the `created()` method,
-move all code from this method to a constructor right after the call to a `super()`
-([example](#assign-dependencies-legacy-element-example)). The `created()`
-method is [deprecated](https://polymer-library.polymer-project.org/2.0/docs/about_20#lifecycle-changes) and is called
-when a super (i.e. base) class constructor is called. If you are unsure about moving the code from the `created` method
-to the class constructor, consult with the source code:
-[`LegacyElementMixin._initializeProperties`](https://github.com/Polymer/polymer/blob/v3.4.0/lib/legacy/legacy-element-mixin.js#L318)
-and
-[`PropertiesChanged.constructor`](https://github.com/Polymer/polymer/blob/v3.4.0/lib/mixins/properties-changed.js#L177)
-
-
-
-**Good:**
-```Javascript
-import {appContext} from `.../services/app-context.js`;
-
-export class MyCustomElement extends ...{
-    constructor() {
-        super(); //This is mandatory to call parent constructor
-        this._userModel = appContext.userModel;
-    }
-    //...
-    _getUserName() {
-        return this._userModel.activeUserName();
-    }
-}
-```
-
-**Bad:**
-```Javascript
-import {appContext} from `.../services/app-context.js`;
-
-export class MyCustomElement extends ...{
-    created() {
-        // Incorrect: assign all dependencies in the constructor
-        this._userModel = appContext.userModel;
-    }
-    //...
-    _getUserName() {
-        // Incorrect: use appContext outside of a constructor
-        return appContext.userModel.activeUserName();
-    }
-}
-```
-
-<a name="assign-dependencies-legacy-element-example"></a>
-**Legacy element:**
-
-Before:
-```Javascript
-export class MyCustomElement extends ...LegacyElementMixin(...) {
-    constructor() {
-        super();
-        someAction();
-    }
-    created() {
-        super();
-        createdAction1();
-        createdAction2();
-    }
-}
-```
-
-After:
-```Javascript
-export class MyCustomElement extends ...LegacyElementMixin(...) {
-    constructor() {
-        super();
-        // Assign services here
-        this._userModel = appContext.userModel;
-        // Code from the created method - put it before existing actions in constructor
-        createdAction1();
-        createdAction2();
-        // Original constructor code
-        someAction();
-    }
-    // created method is removed
-}
-```
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.ts b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.ts
index 01d452a..9d2711b 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.ts
@@ -40,6 +40,8 @@
 import {customElement, property, state} from 'lit/decorators.js';
 import {subscribe} from '../../lit/subscription-controller';
 import {createSearchUrl} from '../../../models/views/search';
+import {userModelToken} from '../../../models/user/user-model';
+import {resolve} from '../../../models/dependency';
 
 const STATES = {
   active: {value: ProjectState.ACTIVE, label: 'Active'},
@@ -110,7 +112,7 @@
 
   @state() private pluginConfigChanged = false;
 
-  private readonly userModel = getAppContext().userModel;
+  private readonly getUserModel = resolve(this, userModelToken);
 
   private readonly restApiService = getAppContext().restApiService;
 
@@ -118,7 +120,7 @@
     super();
     subscribe(
       this,
-      () => this.userModel.preferences$,
+      () => this.getUserModel().preferences$,
       prefs => {
         if (prefs?.download_scheme) {
           // Note (issue 5180): normalize the download scheme with lower-case.
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow.ts
index 5728529..714b588 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow.ts
@@ -39,12 +39,13 @@
 import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
 import {Interaction} from '../../../constants/reporting';
 import {createChangeUrl} from '../../../models/views/change';
+import {userModelToken} from '../../../models/user/user-model';
 
 @customElement('gr-change-list-bulk-vote-flow')
 export class GrChangeListBulkVoteFlow extends LitElement {
   private readonly getBulkActionsModel = resolve(this, bulkActionsModelToken);
 
-  private readonly userModel = getAppContext().userModel;
+  private readonly getUserModel = resolve(this, userModelToken);
 
   private readonly reportingService = getAppContext().reportingService;
 
@@ -141,7 +142,7 @@
     );
     subscribe(
       this,
-      () => this.userModel.account$,
+      () => this.getUserModel().account$,
       account => (this.account = account)
     );
   }
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow.ts
index 436e435..cb6dd7d 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow.ts
@@ -44,6 +44,7 @@
 import {fireAlert, fireReload} from '../../../utils/event-util';
 import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
 import {Interaction} from '../../../constants/reporting';
+import {userModelToken} from '../../../models/user/user-model';
 
 @customElement('gr-change-list-reviewer-flow')
 export class GrChangeListReviewerFlow extends LitElement {
@@ -95,9 +96,11 @@
 
   private readonly reportingService = getAppContext().reportingService;
 
-  private getBulkActionsModel = resolve(this, bulkActionsModelToken);
+  private readonly getBulkActionsModel = resolve(this, bulkActionsModelToken);
 
-  private getConfigModel = resolve(this, configModelToken);
+  private readonly getConfigModel = resolve(this, configModelToken);
+
+  private readonly getUserModel = resolve(this, userModelToken);
 
   private restApiService = getAppContext().restApiService;
 
@@ -169,12 +172,12 @@
     );
     subscribe(
       this,
-      () => getAppContext().userModel.loggedIn$,
+      () => this.getUserModel().loggedIn$,
       isLoggedIn => (this.isLoggedIn = isLoggedIn)
     );
     subscribe(
       this,
-      () => getAppContext().userModel.account$,
+      () => this.getUserModel().account$,
       account => (this.account = account)
     );
   }
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts
index 8000c22..cd5cb96 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts
@@ -27,6 +27,7 @@
 } from '../../../models/views/search';
 import {resolve} from '../../../models/dependency';
 import {subscribe} from '../../lit/subscription-controller';
+import {userModelToken} from '../../../models/user/user-model';
 
 const LIMIT_OPERATOR_PATTERN = /\blimit:(\d+)/i;
 
@@ -76,7 +77,7 @@
 
   private reporting = getAppContext().reportingService;
 
-  private userModel = getAppContext().userModel;
+  private readonly getUserModel = resolve(this, userModelToken);
 
   private readonly getViewModel = resolve(this, searchViewModelToken);
 
@@ -117,22 +118,22 @@
     );
     subscribe(
       this,
-      () => this.userModel.account$,
+      () => this.getUserModel().account$,
       x => (this.account = x)
     );
     subscribe(
       this,
-      () => this.userModel.loggedIn$,
+      () => this.getUserModel().loggedIn$,
       x => (this.loggedIn = x)
     );
     subscribe(
       this,
-      () => this.userModel.preferenceChangesPerPage$,
+      () => this.getUserModel().preferenceChangesPerPage$,
       x => (this.changesPerPage = x)
     );
     subscribe(
       this,
-      () => this.userModel.preferences$,
+      () => this.getUserModel().preferences$,
       x => (this.preferences = x)
     );
   }
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts
index d178880..f37dce3 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts
@@ -57,6 +57,7 @@
   UserDashboard,
   YOUR_TURN,
 } from '../../../utils/dashboard-util';
+import {userModelToken} from '../../../models/user/user-model';
 
 const PROJECT_PLACEHOLDER_PATTERN = /\${project}/g;
 
@@ -107,7 +108,7 @@
 
   private readonly restApiService = getAppContext().restApiService;
 
-  private readonly userModel = getAppContext().userModel;
+  private readonly getUserModel = resolve(this, userModelToken);
 
   private readonly getViewModel = resolve(this, dashboardViewModelToken);
 
@@ -119,7 +120,7 @@
     super();
     subscribe(
       this,
-      () => this.userModel.account$,
+      () => this.getUserModel().account$,
       x => (this.account = x)
     );
     subscribe(
diff --git a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
index ed1f46d..84bdffb 100644
--- a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
@@ -57,6 +57,7 @@
 import {when} from 'lit/directives/when.js';
 import {KnownExperimentId} from '../../../services/flags/flags';
 import {combineLatest} from 'rxjs';
+import {userModelToken} from '../../../models/user/user-model';
 
 function handleSpaceOrEnter(e: KeyboardEvent, handler: () => void) {
   if (modifierPressed(e)) return;
@@ -109,11 +110,9 @@
 
   private readonly showAllChips = new Map<RunStatus | Category, boolean>();
 
-  // private but used in tests
-  readonly getCommentsModel = resolve(this, commentsModelToken);
+  private readonly getCommentsModel = resolve(this, commentsModelToken);
 
-  // private but used in tests
-  readonly userModel = getAppContext().userModel;
+  private readonly getUserModel = resolve(this, userModelToken);
 
   private readonly getChecksModel = resolve(this, checksModelToken);
 
@@ -172,7 +171,7 @@
     );
     subscribe(
       this,
-      () => this.userModel.account$,
+      () => this.getUserModel().account$,
       x => (this.selfAccount = x)
     );
     if (this.flagsService.isEnabled(KnownExperimentId.MENTION_USERS)) {
@@ -180,7 +179,7 @@
         this,
         () =>
           combineLatest([
-            this.userModel.account$,
+            this.getUserModel().account$,
             this.getCommentsModel().threads$,
           ]),
         ([selfAccount, threads]) => {
diff --git a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary_test.ts b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary_test.ts
index 9584637..05036ab 100644
--- a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary_test.ts
@@ -16,11 +16,22 @@
 } from '../../../test/test-data-generators';
 import {stubFlags} from '../../../test/test-utils';
 import {Timestamp} from '../../../api/rest-api';
+import {testResolver} from '../../../test/common-test-setup';
+import {UserModel, userModelToken} from '../../../models/user/user-model';
+import {
+  CommentsModel,
+  commentsModelToken,
+} from '../../../models/comments/comments-model';
 
 suite('gr-change-summary test', () => {
   let element: GrChangeSummary;
+  let commentsModel: CommentsModel;
+  let userModel: UserModel;
+
   setup(async () => {
     element = await fixture(html`<gr-change-summary></gr-change-summary>`);
+    commentsModel = testResolver(commentsModelToken);
+    userModel = testResolver(userModelToken);
   });
 
   test('is defined', () => {
@@ -29,7 +40,7 @@
   });
 
   test('renders', async () => {
-    element.getCommentsModel().setState({
+    commentsModel.setState({
       drafts: {
         a: [createDraft(), createDraft(), createDraft()],
       },
@@ -112,7 +123,7 @@
     element = await fixture(html`<gr-change-summary></gr-change-summary>`);
     await element.updateComplete;
 
-    element.getCommentsModel().setState({
+    commentsModel.setState({
       drafts: {
         a: [
           {
@@ -139,7 +150,7 @@
       },
       discardedDrafts: [],
     });
-    element.userModel.setAccount({
+    userModel.setAccount({
       ...createAccountWithEmail('abc@def.com'),
       registered_on: '2015-03-12 18:32:08.000000000' as Timestamp,
     });
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
index d6e1958..98b0b23 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
@@ -181,6 +181,7 @@
 } from '../../../models/views/change';
 import {rootUrl} from '../../../utils/url-util';
 import {createEditUrl} from '../../../models/views/edit';
+import {userModelToken} from '../../../models/user/user-model';
 
 const MIN_LINES_FOR_COMMIT_COLLAPSE = 18;
 
@@ -535,16 +536,13 @@
 
   private readonly flagsService = getAppContext().flagsService;
 
-  // Private but used in tests.
-  readonly userModel = getAppContext().userModel;
+  private readonly getUserModel = resolve(this, userModelToken);
 
-  // Private but used in tests.
-  readonly getChangeModel = resolve(this, changeModelToken);
+  private readonly getChangeModel = resolve(this, changeModelToken);
 
   private readonly routerModel = getAppContext().routerModel;
 
-  // Private but used in tests.
-  readonly getCommentsModel = resolve(this, commentsModelToken);
+  private readonly getCommentsModel = resolve(this, commentsModelToken);
 
   private readonly getConfigModel = resolve(this, configModelToken);
 
@@ -738,7 +736,7 @@
     );
     subscribe(
       this,
-      () => this.userModel.preferenceDiffViewMode$,
+      () => this.getUserModel().preferenceDiffViewMode$,
       diffViewMode => {
         this.diffViewMode = diffViewMode;
       }
@@ -768,14 +766,14 @@
     );
     subscribe(
       this,
-      () => this.userModel.account$,
+      () => this.getUserModel().account$,
       account => {
         this.account = account;
       }
     );
     subscribe(
       this,
-      () => this.userModel.loggedIn$,
+      () => this.getUserModel().loggedIn$,
       loggedIn => {
         this.loggedIn = loggedIn;
       }
@@ -1726,9 +1724,9 @@
   // Private but used in tests.
   handleToggleDiffMode() {
     if (this.diffViewMode === DiffViewMode.SIDE_BY_SIDE) {
-      this.userModel.updatePreferences({diff_view: DiffViewMode.UNIFIED});
+      this.getUserModel().updatePreferences({diff_view: DiffViewMode.UNIFIED});
     } else {
-      this.userModel.updatePreferences({
+      this.getUserModel().updatePreferences({
         diff_view: DiffViewMode.SIDE_BY_SIDE,
       });
     }
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 f3017f8..095e942 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
@@ -28,7 +28,6 @@
   queryAndAssert,
   stubFlags,
   stubRestApi,
-  stubUsers,
   waitEventLoop,
   waitQueryAndAssert,
   waitUntil,
@@ -86,7 +85,11 @@
 import {ParsedChangeInfo} from '../../../types/types';
 import {GrRelatedChangesList} from '../gr-related-changes-list/gr-related-changes-list';
 import {ChangeStates} from '../../shared/gr-change-status/gr-change-status';
-import {LoadingStatus} from '../../../models/change/change-model';
+import {
+  ChangeModel,
+  changeModelToken,
+  LoadingStatus,
+} from '../../../models/change/change-model';
 import {FocusTarget, GrReplyDialog} from '../gr-reply-dialog/gr-reply-dialog';
 import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
 import {GrChangeStar} from '../../shared/gr-change-star/gr-change-star';
@@ -101,10 +104,18 @@
 import {ChangeViewState} from '../../../models/views/change';
 import {rootUrl} from '../../../utils/url-util';
 import {testResolver} from '../../../test/common-test-setup';
+import {UserModel, userModelToken} from '../../../models/user/user-model';
+import {
+  CommentsModel,
+  commentsModelToken,
+} from '../../../models/comments/comments-model';
 
 suite('gr-change-view tests', () => {
   let element: GrChangeView;
   let setUrlStub: sinon.SinonStub;
+  let userModel: UserModel;
+  let changeModel: ChangeModel;
+  let commentsModel: CommentsModel;
 
   const ROBOT_COMMENTS_LIMIT = 10;
 
@@ -374,6 +385,9 @@
       assertIsDefined(element.actions);
       sinon.stub(element.actions, 'reload').returns(Promise.resolve());
     });
+    userModel = testResolver(userModelToken);
+    commentsModel = testResolver(commentsModelToken);
+    changeModel = testResolver(changeModelToken);
   });
 
   teardown(async () => {
@@ -805,7 +819,7 @@
     });
 
     test('A fires an error event when not logged in', async () => {
-      element.userModel.setAccount(undefined);
+      userModel.setAccount(undefined);
       const loggedInErrorSpy = sinon.spy();
       element.addEventListener('show-auth-required', loggedInErrorSpy);
       pressKey(element, 'a');
@@ -978,14 +992,14 @@
     });
 
     test('m should toggle diff mode', async () => {
-      const updatePreferencesStub = stubUsers('updatePreferences');
+      const updatePreferencesStub = sinon.stub(userModel, 'updatePreferences');
       await element.updateComplete;
 
       const prefs = {
         ...createDefaultPreferences(),
         diff_view: DiffViewMode.SIDE_BY_SIDE,
       };
-      element.userModel.setPreferences(prefs);
+      userModel.setPreferences(prefs);
       element.handleToggleDiffMode();
       assert.isTrue(
         updatePreferencesStub.calledWith({diff_view: DiffViewMode.UNIFIED})
@@ -995,7 +1009,7 @@
         ...createDefaultPreferences(),
         diff_view: DiffViewMode.UNIFIED,
       };
-      element.userModel.setPreferences(newPrefs);
+      userModel.setPreferences(newPrefs);
       await element.updateComplete;
       element.handleToggleDiffMode();
       assert.isTrue(
@@ -1586,11 +1600,11 @@
     sinon.stub(element, 'loadAndSetCommitInfo');
     await element.updateComplete;
     const reloadPortedCommentsStub = sinon.stub(
-      element.getCommentsModel(),
+      commentsModel,
       'reloadPortedComments'
     );
     const reloadPortedDraftsStub = sinon.stub(
-      element.getCommentsModel(),
+      commentsModel,
       'reloadPortedDrafts'
     );
     sinon.stub(element.fileList, 'collapseAllDiffs');
@@ -1683,7 +1697,7 @@
     );
 
     element.viewState = createChangeViewState();
-    element.getChangeModel().setState({
+    changeModel.setState({
       loadingStatus: LoadingStatus.LOADED,
       change: {
         ...createChangeViewChange(),
@@ -1776,7 +1790,7 @@
 
   test('topic is coalesced to null', async () => {
     sinon.stub(element, 'changeChanged');
-    element.getChangeModel().setState({
+    changeModel.setState({
       loadingStatus: LoadingStatus.LOADED,
       change: {
         ...createChangeViewChange(),
@@ -1791,7 +1805,7 @@
   });
 
   test('commit sha is populated from getChangeDetail', async () => {
-    element.getChangeModel().setState({
+    changeModel.setState({
       loadingStatus: LoadingStatus.LOADED,
       change: {
         ...createChangeViewChange(),
@@ -2161,7 +2175,7 @@
   test('selectedRevision updates when patchNum is changed', async () => {
     const revision1: RevisionInfo = createRevision(1);
     const revision2: RevisionInfo = createRevision(2);
-    element.getChangeModel().setState({
+    changeModel.setState({
       loadingStatus: LoadingStatus.LOADED,
       change: {
         ...createChangeViewChange(),
@@ -2174,7 +2188,7 @@
         current_revision: 'bbb' as CommitId,
       },
     });
-    element.userModel.setPreferences(createPreferences());
+    userModel.setPreferences(createPreferences());
 
     element.patchRange = {patchNum: 2 as RevisionPatchSetNum};
     await element.performPostChangeLoadTasks();
@@ -2189,7 +2203,7 @@
     const revision1 = createRevision(1);
     const revision2 = createRevision(2);
     const revision3 = createEditRevision();
-    element.getChangeModel().setState({
+    changeModel.setState({
       loadingStatus: LoadingStatus.LOADED,
       change: {
         ...createChangeViewChange(),
@@ -2463,7 +2477,7 @@
         changeNum: TEST_NUMERIC_CHANGE_ID,
         project: TEST_PROJECT_NAME,
       };
-      element.getChangeModel().setState({
+      changeModel.setState({
         loadingStatus: LoadingStatus.LOADED,
         change: {
           ...createChangeViewChange(),
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.ts b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.ts
index 832738b..9bac7c2 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.ts
@@ -38,10 +38,10 @@
   shortcutsServiceToken,
 } from '../../../services/shortcuts/shortcuts-service';
 import {resolve} from '../../../models/dependency';
-import {getAppContext} from '../../../services/app-context';
 import {subscribe} from '../../lit/subscription-controller';
 import {configModelToken} from '../../../models/config/config-model';
 import {createChangeUrl} from '../../../models/views/change';
+import {userModelToken} from '../../../models/user/user-model';
 
 @customElement('gr-file-list-header')
 export class GrFileListHeader extends LitElement {
@@ -123,7 +123,7 @@
   // 'hide diffs' buttons still be functional.
   private readonly maxFilesForBulkActions = 225;
 
-  private readonly userModel = getAppContext().userModel;
+  private readonly getUserModel = resolve(this, userModelToken);
 
   private readonly getNavigation = resolve(this, navigationToken);
 
@@ -131,7 +131,7 @@
     super();
     subscribe(
       this,
-      () => this.userModel.diffPreferences$,
+      () => this.getUserModel().diffPreferences$,
       diffPreferences => {
         if (!diffPreferences) return;
         this.diffPrefs = diffPreferences;
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
index 3e8530c..c40d018 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
@@ -76,6 +76,7 @@
 import {createDiffUrl} from '../../../models/views/diff';
 import {createEditUrl} from '../../../models/views/edit';
 import {createChangeUrl} from '../../../models/views/change';
+import {userModelToken} from '../../../models/user/user-model';
 
 export const DEFAULT_NUM_FILES_SHOWN = 200;
 
@@ -285,7 +286,7 @@
 
   private readonly restApiService = getAppContext().restApiService;
 
-  private readonly userModel = getAppContext().userModel;
+  private readonly getUserModel = resolve(this, userModelToken);
 
   private readonly getChangeModel = resolve(this, changeModelToken);
 
@@ -766,7 +767,7 @@
     );
     subscribe(
       this,
-      () => this.userModel.diffPreferences$,
+      () => this.getUserModel().diffPreferences$,
       diffPreferences => {
         this.diffPrefs = diffPreferences;
       }
@@ -775,7 +776,7 @@
       this,
       () =>
         select(
-          this.userModel.preferences$,
+          this.getUserModel().preferences$,
           prefs => !!prefs?.size_bar_in_change_table
         ),
       sizeBarInChangeTable => {
@@ -784,7 +785,7 @@
     );
     subscribe(
       this,
-      () => this.userModel.loggedIn$,
+      () => this.getUserModel().loggedIn$,
       loggedIn => {
         this.loggedIn = loggedIn;
       }
@@ -2595,7 +2596,7 @@
   }
 
   private handleReloadingDiffPreference() {
-    this.userModel.getDiffPreferences();
+    this.getUserModel().getDiffPreferences();
   }
 
   private getOldPath(file: NormalizedFileInfo) {
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts
index 5417127..e7a54fb 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts
@@ -322,8 +322,7 @@
   @state()
   private combinedMessages: CombinedMessage[] = [];
 
-  // Private but used in tests.
-  readonly getCommentsModel = resolve(this, commentsModelToken);
+  private readonly getCommentsModel = resolve(this, commentsModelToken);
 
   private readonly changeModel = resolve(this, changeModelToken);
 
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.ts b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.ts
index 2e62718..158ad8d 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.ts
@@ -32,6 +32,8 @@
 import {fixture, assert} from '@open-wc/testing';
 import {GrButton} from '../../shared/gr-button/gr-button';
 import {PaperToggleButtonElement} from '@polymer/paper-toggle-button';
+import {testResolver} from '../../../test/common-test-setup';
+import {commentsModelToken} from '../../../models/comments/comments-model';
 
 const author = {
   _account_id: 42 as AccountId,
@@ -136,7 +138,9 @@
       element = await fixture<GrMessagesList>(
         html`<gr-messages-list></gr-messages-list>`
       );
-      await element.getCommentsModel().reloadComments(0 as NumericChangeId);
+      await testResolver(commentsModelToken).reloadComments(
+        0 as NumericChangeId
+      );
       element.messages = messages;
       await element.updateComplete;
     });
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
index 26b9a63..ba3e62f 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
@@ -131,6 +131,7 @@
 import {ShortcutController} from '../../lit/shortcut-controller';
 import {Key, Modifier, whenVisible} from '../../../utils/dom-util';
 import {GrThreadList} from '../gr-thread-list/gr-thread-list';
+import {userModelToken} from '../../../models/user/user-model';
 
 export enum FocusTarget {
   ANY = 'any',
@@ -216,8 +217,7 @@
 
   private readonly getChangeModel = resolve(this, changeModelToken);
 
-  // Private but used in tests.
-  readonly getCommentsModel = resolve(this, commentsModelToken);
+  private readonly getCommentsModel = resolve(this, commentsModelToken);
 
   // TODO: update type to only ParsedChangeInfo
   @property({type: Object})
@@ -397,6 +397,8 @@
 
   private readonly accountsModel = getAppContext().accountsModel;
 
+  private readonly getUserModel = resolve(this, userModelToken);
+
   private latestPatchNum?: PatchSetNumber;
 
   storeTask?: DelayedTask;
@@ -631,7 +633,7 @@
 
     subscribe(
       this,
-      () => getAppContext().userModel.loggedIn$,
+      () => this.getUserModel().loggedIn$,
       isLoggedIn => (this.isLoggedIn = isLoggedIn)
     );
     subscribe(
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
index 8185139..ef781ba 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
@@ -63,6 +63,11 @@
 import {KnownExperimentId} from '../../../services/flags/flags';
 import {Key, Modifier} from '../../../utils/dom-util';
 import {GrComment} from '../../shared/gr-comment/gr-comment';
+import {testResolver} from '../../../test/common-test-setup';
+import {
+  CommentsModel,
+  commentsModelToken,
+} from '../../../models/comments/comments-model';
 
 function cloneableResponse(status: number, text: string) {
   return {
@@ -88,6 +93,7 @@
   let element: GrReplyDialog;
   let changeNum: NumericChangeId;
   let patchNum: PatchSetNum;
+  let commentsModel: CommentsModel;
 
   let lastId = 1;
   const makeAccount = function () {
@@ -148,6 +154,7 @@
     element.draftCommentThreads = [];
 
     await element.updateComplete;
+    commentsModel = testResolver(commentsModelToken);
   });
 
   function stubSaveReview(
@@ -2378,7 +2385,7 @@
 
     test('replies to patchset level comments are not filtered out', async () => {
       const draft = {...createDraft(), in_reply_to: '1' as UrlEncodedCommentId};
-      element.getCommentsModel().setState({
+      commentsModel.setState({
         drafts: {
           'abc.txt': [draft],
         },
@@ -2414,7 +2421,7 @@
         ...createDraft(),
         message: 'hey @abcd@def take a look at this',
       };
-      element.getCommentsModel().setState({
+      commentsModel.setState({
         comments: {},
         robotComments: {},
         drafts: {
@@ -2449,7 +2456,7 @@
         message: 'hey @abcd@def.com take a look at this',
         unresolved: true,
       };
-      element.getCommentsModel().setState({
+      commentsModel.setState({
         comments: {},
         robotComments: {},
         drafts: {
@@ -2489,7 +2496,7 @@
         message: 'hey @abcd@def.com take a look at this',
         unresolved: true,
       };
-      element.getCommentsModel().setState({
+      commentsModel.setState({
         comments: {},
         robotComments: {},
         drafts: {
@@ -2542,7 +2549,7 @@
       };
       stubRestApi('getAccountDetails').returns(Promise.resolve(account));
 
-      element.getCommentsModel().setState({
+      commentsModel.setState({
         comments: {},
         robotComments: {},
         drafts: {
@@ -2579,7 +2586,7 @@
         registered_on: '2015-03-12 18:32:08.000000000' as Timestamp,
       };
       stubRestApi('getAccountDetails').returns(Promise.resolve(account));
-      element.getCommentsModel().setState({
+      commentsModel.setState({
         comments: {},
         robotComments: {},
         drafts: {
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts
index 27b5097..414fed9 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts
@@ -44,6 +44,7 @@
 import {Interaction} from '../../../constants/reporting';
 import {KnownExperimentId} from '../../../services/flags/flags';
 import {HtmlPatched} from '../../../utils/lit-util';
+import {userModelToken} from '../../../models/user/user-model';
 
 enum SortDropdownState {
   TIMESTAMP = 'Latest timestamp',
@@ -205,7 +206,7 @@
 
   private readonly flagsService = getAppContext().flagsService;
 
-  private readonly userModel = getAppContext().userModel;
+  private readonly getUserModel = resolve(this, userModelToken);
 
   private readonly patched = new HtmlPatched(key => {
     this.reporting.reportInteraction(Interaction.AUTOCLOSE_HTML_PATCHED, {
@@ -228,7 +229,7 @@
     );
     subscribe(
       this,
-      () => this.userModel.account$,
+      () => this.getUserModel().account$,
       x => (this.account = x)
     );
     // for COMMENTS_AUTOCLOSE logging purposes only
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts
index dcc9a99..6c6d6a0 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts
@@ -29,6 +29,7 @@
 import {fireEvent} from '../../../utils/event-util';
 import {resolve} from '../../../models/dependency';
 import {configModelToken} from '../../../models/config/config-model';
+import {userModelToken} from '../../../models/user/user-model';
 
 type MainHeaderLink = RequireProperties<DropdownLink, 'url' | 'name'>;
 
@@ -142,9 +143,9 @@
 
   private readonly jsAPI = getAppContext().jsApiService;
 
-  private readonly userModel = getAppContext().userModel;
+  private readonly getUserModel = resolve(this, userModelToken);
 
-  private readonly configModel = resolve(this, configModelToken);
+  private readonly getConfigModel = resolve(this, configModelToken);
 
   private subscriptions: Subscription[] = [];
 
@@ -153,8 +154,8 @@
     this.loadAccount();
 
     this.subscriptions.push(
-      this.userModel.preferences$
-        .pipe(
+      this.getUserModel()
+        .preferences$.pipe(
           map(preferences => preferences?.my ?? []),
           distinctUntilChanged()
         )
@@ -163,7 +164,7 @@
         })
     );
     this.subscriptions.push(
-      this.configModel().serverConfig$.subscribe(config => {
+      this.getConfigModel().serverConfig$.subscribe(config => {
         if (!config) return;
         this.serverConfig = config;
         this.retrieveFeedbackURL(config);
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
index 17d7516..8e77f96 100644
--- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
+++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
@@ -34,6 +34,7 @@
 import {resolve} from '../../../models/dependency';
 import {createChangeUrl} from '../../../models/views/change';
 import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
+import {userModelToken} from '../../../models/user/user-model';
 
 interface FilePreview {
   filepath: string;
@@ -89,7 +90,7 @@
 
   private readonly restApiService = getAppContext().restApiService;
 
-  private readonly userModel = getAppContext().userModel;
+  private readonly getUserModel = resolve(this, userModelToken);
 
   private readonly getNavigation = resolve(this, navigationToken);
 
@@ -97,7 +98,7 @@
     super();
     subscribe(
       this,
-      () => this.userModel.preferences$,
+      () => this.getUserModel().preferences$,
       preferences => {
         if (!preferences?.disable_token_highlighting) {
           this.layers = [new TokenHighlightLayer(this)];
@@ -106,7 +107,7 @@
     );
     subscribe(
       this,
-      () => this.userModel.diffPreferences$,
+      () => this.getUserModel().diffPreferences$,
       diffPreferences => {
         if (!diffPreferences) return;
         this.diffPrefs = diffPreferences;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
index baf89c0..35eebd0 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
@@ -95,6 +95,7 @@
 } from '../../../utils/async-util';
 import {subscribe} from '../../lit/subscription-controller';
 import {GeneratedWebLink} from '../../../utils/weblink-util';
+import {userModelToken} from '../../../models/user/user-model';
 
 const EMPTY_BLAME = 'No blame information for this diff.';
 
@@ -330,7 +331,7 @@
   private readonly restApiService = getAppContext().restApiService;
 
   // visible for testing
-  readonly userModel = getAppContext().userModel;
+  readonly getUserModel = resolve(this, userModelToken);
 
   // visible for testing
   readonly jsAPI = getAppContext().jsApiService;
@@ -372,7 +373,7 @@
     );
     subscribe(
       this,
-      () => this.userModel.loggedIn$,
+      () => this.getUserModel().loggedIn$,
       loggedIn => (this.loggedIn = loggedIn)
     );
     subscribe(
@@ -384,7 +385,7 @@
     );
     subscribe(
       this,
-      () => this.userModel.diffPreferences$,
+      () => this.getUserModel().diffPreferences$,
       diffPreferences => {
         this.prefs = diffPreferences;
       }
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.ts b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.ts
index 598819b..4163989 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.ts
@@ -53,11 +53,14 @@
 import {GrAnnotationActionsInterface} from '../../shared/gr-js-api-interface/gr-annotation-actions-js-api';
 import {fixture, html, assert} from '@open-wc/testing';
 import {EventType} from '../../../types/events';
+import {testResolver} from '../../../test/common-test-setup';
+import {userModelToken, UserModel} from '../../../models/user/user-model';
 
 suite('gr-diff-host tests', () => {
   let element: GrDiffHost;
   let account = createAccountDetailWithId(1);
   let getDiffRestApiStub: SinonStub;
+  let userModel: UserModel;
 
   setup(async () => {
     stubRestApi('getAccount').callsFake(() => Promise.resolve(account));
@@ -70,6 +73,7 @@
     // Fall back in case a test forgets to set one up
     getDiffRestApiStub.returns(Promise.resolve(createDiff()));
     await element.updateComplete;
+    userModel = testResolver(userModelToken);
   });
 
   suite('plugin layers', () => {
@@ -591,7 +595,7 @@
   });
 
   test('cannot create comments when not logged in', () => {
-    element.userModel.setAccount(undefined);
+    userModel.setAccount(undefined);
     element.patchRange = createPatchRange();
     const showAuthRequireSpy = sinon.spy();
     element.addEventListener('show-auth-required', showAuthRequireSpy);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
index 6ad8e2f..9a8a9ab 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
@@ -100,7 +100,6 @@
 import {LoadingStatus} from '../../../models/change/change-model';
 import {DisplayLine} from '../../../api/diff';
 import {GrDownloadDialog} from '../../change/gr-download-dialog/gr-download-dialog';
-import {browserModelToken} from '../../../models/browser/browser-model';
 import {commentsModelToken} from '../../../models/comments/comments-model';
 import {changeModelToken} from '../../../models/change/change-model';
 import {resolve} from '../../../models/dependency';
@@ -122,6 +121,7 @@
 import {createChangeUrl} from '../../../models/views/change';
 import {createEditUrl} from '../../../models/views/edit';
 import {GeneratedWebLink} from '../../../utils/weblink-util';
+import {userModelToken} from '../../../models/user/user-model';
 
 const LOADING_BLAME = 'Loading blame...';
 const LOADED_BLAME = 'Blame loaded';
@@ -291,17 +291,11 @@
   // Private but used in tests.
   readonly routerModel = getAppContext().routerModel;
 
-  // Private but used in tests.
-  readonly userModel = getAppContext().userModel;
+  private readonly getUserModel = resolve(this, userModelToken);
 
-  // Private but used in tests.
-  readonly getChangeModel = resolve(this, changeModelToken);
+  private readonly getChangeModel = resolve(this, changeModelToken);
 
-  // Private but used in tests.
-  readonly getBrowserModel = resolve(this, browserModelToken);
-
-  // Private but used in tests.
-  readonly getCommentsModel = resolve(this, commentsModelToken);
+  private readonly getCommentsModel = resolve(this, commentsModelToken);
 
   private readonly getShortcutsService = resolve(this, shortcutsServiceToken);
 
@@ -396,7 +390,7 @@
   private setupSubscriptions() {
     subscribe(
       this,
-      () => this.userModel.loggedIn$,
+      () => this.getUserModel().loggedIn$,
       loggedIn => {
         this.loggedIn = loggedIn;
       }
@@ -417,14 +411,14 @@
     );
     subscribe(
       this,
-      () => this.userModel.preferences$,
+      () => this.getUserModel().preferences$,
       preferences => {
         this.userPrefs = preferences;
       }
     );
     subscribe(
       this,
-      () => this.userModel.diffPreferences$,
+      () => this.getUserModel().diffPreferences$,
       diffPreferences => {
         this.prefs = diffPreferences;
       }
@@ -479,7 +473,7 @@
             combineLatest([
               this.getChangeModel().patchNum$,
               this.routerModel.routerView$,
-              this.userModel.diffPreferences$,
+              this.getUserModel().diffPreferences$,
               this.getChangeModel().reviewedFiles$,
             ]).pipe(
               filter(
@@ -1315,9 +1309,9 @@
   handleToggleDiffMode() {
     if (!this.userPrefs) return;
     if (this.userPrefs.diff_view === DiffViewMode.SIDE_BY_SIDE) {
-      this.userModel.updatePreferences({diff_view: DiffViewMode.UNIFIED});
+      this.getUserModel().updatePreferences({diff_view: DiffViewMode.UNIFIED});
     } else {
-      this.userModel.updatePreferences({
+      this.getUserModel().updatePreferences({
         diff_view: DiffViewMode.SIDE_BY_SIDE,
       });
     }
@@ -2266,7 +2260,7 @@
   }
 
   private handleReloadingDiffPreference() {
-    this.userModel.getDiffPreferences();
+    this.getUserModel().getDiffPreferences();
   }
 
   private computeCanEdit() {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.ts b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.ts
index b6e26ab..2141224 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.ts
@@ -20,7 +20,6 @@
   queryAndAssert,
   stubReporting,
   stubRestApi,
-  stubUsers,
   waitEventLoop,
   waitUntil,
 } from '../../../test/test-utils';
@@ -60,7 +59,11 @@
 import {Files, GrDiffView} from './gr-diff-view';
 import {DropdownItem} from '../../shared/gr-dropdown-list/gr-dropdown-list';
 import {SinonFakeTimers, SinonStub, SinonSpy} from 'sinon';
-import {LoadingStatus} from '../../../models/change/change-model';
+import {
+  changeModelToken,
+  ChangeModel,
+  LoadingStatus,
+} from '../../../models/change/change-model';
 import {CommentMap} from '../../../utils/comment-util';
 import {ParsedChangeInfo} from '../../../types/types';
 import {assertIsDefined} from '../../../utils/common-util';
@@ -70,6 +73,15 @@
 import {Key} from '../../../utils/dom-util';
 import {GrButton} from '../../shared/gr-button/gr-button';
 import {testResolver} from '../../../test/common-test-setup';
+import {UserModel, userModelToken} from '../../../models/user/user-model';
+import {
+  commentsModelToken,
+  CommentsModel,
+} from '../../../models/comments/comments-model';
+import {
+  BrowserModel,
+  browserModelToken,
+} from '../../../models/browser/browser-model';
 
 function createComment(
   id: string,
@@ -93,6 +105,10 @@
     let diffCommentsStub;
     let getDiffRestApiStub: SinonStub;
     let setUrlStub: SinonStub;
+    let changeModel: ChangeModel;
+    let commentsModel: CommentsModel;
+    let browserModel: BrowserModel;
+    let userModel: UserModel;
 
     function getFilesFromFileList(fileList: string[]): Files {
       const changeFilesByPath = fileList.reduce((files, path) => {
@@ -140,8 +156,12 @@
         ],
       });
       await element.updateComplete;
+      commentsModel = testResolver(commentsModelToken);
+      changeModel = testResolver(changeModelToken);
+      browserModel = testResolver(browserModelToken);
+      userModel = testResolver(userModelToken);
 
-      element.getCommentsModel().setState({
+      commentsModel.setState({
         comments: {},
         robotComments: {},
         drafts: {},
@@ -192,7 +212,7 @@
         assertIsDefined(element.diffHost);
         sinon.stub(element.diffHost, 'reload').returns(Promise.resolve());
         viewStateChangedSpy = sinon.spy(element, 'viewStateChanged');
-        element.getChangeModel().setState({
+        changeModel.setState({
           change: {
             ...createParsedChange(),
             revisions: createRevisions(11),
@@ -202,7 +222,7 @@
       });
 
       test('comment url resolves to comment.patch_set vs latest', () => {
-        element.getCommentsModel().setState({
+        commentsModel.setState({
           comments: {
             '/COMMIT_MSG': [
               createComment('c1', 10, 2, '/COMMIT_MSG'),
@@ -265,7 +285,7 @@
     });
 
     test('unchanged diff X vs latest from comment links navigates to base vs X', async () => {
-      element.getCommentsModel().setState({
+      commentsModel.setState({
         comments: {
           '/COMMIT_MSG': [
             createComment('c1', 10, 2, '/COMMIT_MSG'),
@@ -284,7 +304,7 @@
       sinon.stub(element.diffHost, 'reload').returns(Promise.resolve());
       sinon.stub(element, 'isFileUnchanged').returns(true);
       const viewStateChangedSpy = sinon.spy(element, 'viewStateChanged');
-      element.getChangeModel().setState({
+      changeModel.setState({
         change: {
           ...createParsedChange(),
           revisions: createRevisions(11),
@@ -311,7 +331,7 @@
     });
 
     test('unchanged diff Base vs latest from comment does not navigate', async () => {
-      element.getCommentsModel().setState({
+      commentsModel.setState({
         comments: {
           '/COMMIT_MSG': [
             createComment('c1', 10, 2, '/COMMIT_MSG'),
@@ -330,7 +350,7 @@
       sinon.stub(element.diffHost, 'reload').returns(Promise.resolve());
       sinon.stub(element, 'isFileUnchanged').returns(true);
       const viewStateChangedSpy = sinon.spy(element, 'viewStateChanged');
-      element.getChangeModel().setState({
+      changeModel.setState({
         change: {
           ...createParsedChange(),
           revisions: createRevisions(11),
@@ -385,7 +405,7 @@
     });
 
     test('diff toast to go to latest is shown and not base', async () => {
-      element.getCommentsModel().setState({
+      commentsModel.setState({
         comments: {
           '/COMMIT_MSG': [
             createComment('c1', 10, 2, '/COMMIT_MSG'),
@@ -405,7 +425,7 @@
       sinon.stub(element.diffHost, 'reload').returns(Promise.resolve());
       const viewStateChangedSpy = sinon.spy(element, 'viewStateChanged');
       element.change = undefined;
-      element.getChangeModel().setState({
+      changeModel.setState({
         change: {
           ...createParsedChange(),
           revisions: createRevisions(11),
@@ -439,7 +459,7 @@
     test('renders', async () => {
       clock = sinon.useFakeTimers();
       element.changeNum = 42 as NumericChangeId;
-      element.getBrowserModel().setScreenWidth(0);
+      browserModel.setScreenWidth(0);
       element.patchRange = {
         basePatchNum: PARENT,
         patchNum: 10 as RevisionPatchSetNum,
@@ -623,7 +643,7 @@
     test('keyboard shortcuts', async () => {
       clock = sinon.useFakeTimers();
       element.changeNum = 42 as NumericChangeId;
-      element.getBrowserModel().setScreenWidth(0);
+      browserModel.setScreenWidth(0);
       element.patchRange = {
         basePatchNum: PARENT,
         patchNum: 10 as RevisionPatchSetNum,
@@ -1529,7 +1549,7 @@
         'automatically called',
       async () => {
         const setReviewedFileStatusStub = sinon
-          .stub(element.getChangeModel(), 'setReviewedFilesStatus')
+          .stub(changeModel, 'setReviewedFilesStatus')
           .callsFake(() => Promise.resolve());
 
         const setReviewedStatusStub = sinon.spy(element, 'setReviewedStatus');
@@ -1541,8 +1561,8 @@
           ...createDefaultDiffPrefs(),
           manual_review: true,
         };
-        element.userModel.setDiffPreferences(diffPreferences);
-        element.getChangeModel().setState({
+        userModel.setDiffPreferences(diffPreferences);
+        changeModel.setState({
           change: createParsedChange(),
           diffPath: '/COMMIT_MSG',
           reviewedFiles: [],
@@ -1564,7 +1584,7 @@
         assert.isFalse(setReviewedFileStatusStub.called);
 
         // if prefs are updated then the reviewed status should not be set again
-        element.userModel.setDiffPreferences(createDefaultDiffPrefs());
+        userModel.setDiffPreferences(createDefaultDiffPrefs());
 
         await element.updateComplete;
         assert.isFalse(setReviewedFileStatusStub.called);
@@ -1573,7 +1593,7 @@
 
     test('_prefs.manual_review false means set reviewed is called', async () => {
       const setReviewedFileStatusStub = sinon
-        .stub(element.getChangeModel(), 'setReviewedFilesStatus')
+        .stub(changeModel, 'setReviewedFilesStatus')
         .callsFake(() => Promise.resolve());
 
       assertIsDefined(element.diffHost);
@@ -1583,8 +1603,8 @@
         ...createDefaultDiffPrefs(),
         manual_review: false,
       };
-      element.userModel.setDiffPreferences(diffPreferences);
-      element.getChangeModel().setState({
+      userModel.setDiffPreferences(diffPreferences);
+      changeModel.setState({
         change: createParsedChange(),
         diffPath: '/COMMIT_MSG',
         reviewedFiles: [],
@@ -1607,7 +1627,7 @@
     });
 
     test('file review status', async () => {
-      element.getChangeModel().setState({
+      changeModel.setState({
         change: createParsedChange(),
         diffPath: '/COMMIT_MSG',
         reviewedFiles: [],
@@ -1615,12 +1635,12 @@
       });
       element.loggedIn = true;
       const saveReviewedStub = sinon
-        .stub(element.getChangeModel(), 'setReviewedFilesStatus')
+        .stub(changeModel, 'setReviewedFilesStatus')
         .callsFake(() => Promise.resolve());
       assertIsDefined(element.diffHost);
       sinon.stub(element.diffHost, 'reload');
 
-      element.userModel.setDiffPreferences(createDefaultDiffPrefs());
+      userModel.setDiffPreferences(createDefaultDiffPrefs());
 
       element.routerModel.setState({
         changeNum: TEST_NUMERIC_CHANGE_ID,
@@ -1635,7 +1655,7 @@
 
       await waitUntil(() => saveReviewedStub.called);
 
-      element.getChangeModel().updateStateFileReviewed('/COMMIT_MSG', true);
+      changeModel.updateStateFileReviewed('/COMMIT_MSG', true);
       await element.updateComplete;
 
       const reviewedStatusCheckBox = queryAndAssert<HTMLInputElement>(
@@ -1660,7 +1680,7 @@
         false,
       ]);
 
-      element.getChangeModel().updateStateFileReviewed('/COMMIT_MSG', false);
+      changeModel.updateStateFileReviewed('/COMMIT_MSG', false);
       await element.updateComplete;
 
       reviewedStatusCheckBox.click();
@@ -1688,7 +1708,7 @@
 
     test('file review status with edit loaded', async () => {
       const saveReviewedStub = sinon.stub(
-        element.getChangeModel(),
+        changeModel,
         'setReviewedFilesStatus'
       );
 
@@ -1730,9 +1750,9 @@
         ...createDefaultPreferences(),
         diff_view: DiffViewMode.SIDE_BY_SIDE,
       };
-      element.getBrowserModel().setScreenWidth(0);
+      browserModel.setScreenWidth(0);
 
-      const userStub = stubUsers('updatePreferences');
+      const userStub = sinon.stub(userModel, 'updatePreferences');
 
       await element.updateComplete;
       // The mode selected in the view state reflects the selected option.
@@ -1926,7 +1946,7 @@
     });
 
     test('handleToggleDiffMode', () => {
-      const userStub = stubUsers('updatePreferences');
+      const userStub = sinon.stub(userModel, 'updatePreferences');
       element.userPrefs = {
         ...createDefaultPreferences(),
         diff_view: DiffViewMode.SIDE_BY_SIDE,
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
index a15a575..e245ff5 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
@@ -33,6 +33,7 @@
 import {ShortcutController} from '../../lit/shortcut-controller';
 import {editViewModelToken, EditViewState} from '../../../models/views/edit';
 import {createChangeUrl} from '../../../models/views/change';
+import {userModelToken} from '../../../models/user/user-model';
 
 const RESTORED_MESSAGE = 'Content restored from a previous edit.';
 const SAVING_MESSAGE = 'Saving changes...';
@@ -89,7 +90,7 @@
 
   private readonly reporting = getAppContext().reportingService;
 
-  private readonly userModel = getAppContext().userModel;
+  private readonly getUserModel = resolve(this, userModelToken);
 
   private readonly getChangeModel = resolve(this, changeModelToken);
 
@@ -109,7 +110,7 @@
     });
     subscribe(
       this,
-      () => this.userModel.editPreferences$,
+      () => this.getUserModel().editPreferences$,
       editPreferences => (this.editPrefs = editPreferences)
     );
     subscribe(
diff --git a/polygerrit-ui/app/elements/gr-app-element.ts b/polygerrit-ui/app/elements/gr-app-element.ts
index c05e4c4..27d2857 100644
--- a/polygerrit-ui/app/elements/gr-app-element.ts
+++ b/polygerrit-ui/app/elements/gr-app-element.ts
@@ -73,6 +73,7 @@
 import {createSearchUrl, SearchViewState} from '../models/views/search';
 import {createSettingsUrl} from '../models/views/settings';
 import {createDashboardUrl} from '../models/views/dashboard';
+import {userModelToken} from '../models/user/user-model';
 
 interface ErrorInfo {
   text: string;
@@ -161,7 +162,7 @@
 
   private readonly shortcuts = new ShortcutController(this);
 
-  private readonly userModel = getAppContext().userModel;
+  private readonly getUserModel = resolve(this, userModelToken);
 
   private readonly routerModel = getAppContext().routerModel;
 
@@ -210,7 +211,7 @@
 
     subscribe(
       this,
-      () => this.userModel.preferenceTheme$,
+      () => this.getUserModel().preferenceTheme$,
       theme => {
         this.theme = theme;
         this.applyTheme();
@@ -257,14 +258,14 @@
     // TODO(milutin): Remove saving preferences after while. This code is
     // for migration.
     if (window.localStorage.getItem('dark-theme')) {
-      this.userModel.updatePreferences({theme: AppTheme.DARK});
+      this.getUserModel().updatePreferences({theme: AppTheme.DARK});
       window.localStorage.removeItem('dark-theme');
       this.reporting.reportExecution(
         Execution.REACHABLE_CODE,
         'Dark theme was migrated from localstorage'
       );
     } else if (window.localStorage.getItem('light-theme')) {
-      this.userModel.updatePreferences({theme: AppTheme.LIGHT});
+      this.getUserModel().updatePreferences({theme: AppTheme.LIGHT});
       window.localStorage.removeItem('light-theme');
       this.reporting.reportExecution(
         Execution.REACHABLE_CODE,
diff --git a/polygerrit-ui/app/elements/gr-app.ts b/polygerrit-ui/app/elements/gr-app.ts
index 2f7dfac..721b46d 100644
--- a/polygerrit-ui/app/elements/gr-app.ts
+++ b/polygerrit-ui/app/elements/gr-app.ts
@@ -47,6 +47,7 @@
 import {html, LitElement} from 'lit';
 import {customElement} from 'lit/decorators.js';
 import {ServiceWorkerInstaller} from '../services/service-worker-installer';
+import {userModelToken} from '../models/user/user-model';
 
 const appContext = createAppContext();
 injectAppContext(appContext);
@@ -107,7 +108,7 @@
       this.serviceWorkerInstaller = new ServiceWorkerInstaller(
         appContext.flagsService,
         appContext.reportingService,
-        appContext.userModel
+        resolver(userModelToken)
       );
     }
   }
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.ts b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.ts
index b0993b9..2add2bb 100644
--- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.ts
@@ -16,8 +16,7 @@
   @state()
   config?: ServerInfo;
 
-  // visible for testing
-  readonly getConfigModel = resolve(this, configModelToken);
+  private readonly getConfigModel = resolve(this, configModelToken);
 
   constructor() {
     super();
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.ts b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.ts
index bb89d12..a58314e 100644
--- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.ts
@@ -10,10 +10,16 @@
 import {fixture, html, assert} from '@open-wc/testing';
 import {SinonStub} from 'sinon';
 import {createServerInfo} from '../../../test/test-data-generators';
+import {
+  ConfigModel,
+  configModelToken,
+} from '../../../models/config/config-model';
+import {testResolver} from '../../../test/common-test-setup';
 
 suite('gr-plugin-host tests', () => {
   let element: GrPluginHost;
   let loadPluginsStub: SinonStub;
+  let configModel: ConfigModel;
 
   setup(async () => {
     loadPluginsStub = sinon.stub(getPluginLoader(), 'loadPlugins');
@@ -21,13 +27,14 @@
       <gr-plugin-host></gr-plugin-host>
     `);
     await element.updateComplete;
+    configModel = testResolver(configModelToken);
 
     sinon.stub(document.body, 'appendChild');
   });
 
   test('load plugins should be called', async () => {
     loadPluginsStub.reset();
-    element.getConfigModel().updateServerConfig({
+    configModel.updateServerConfig({
       ...createServerInfo(),
       plugin: {
         has_avatars: false,
@@ -46,7 +53,7 @@
 
   test('theme plugins should be loaded if enabled', async () => {
     loadPluginsStub.reset();
-    element.getConfigModel().updateServerConfig({
+    configModel.updateServerConfig({
       ...createServerInfo(),
       default_theme: 'gerrit-theme.js',
       plugin: {
@@ -69,7 +76,7 @@
     loadPluginsStub.reset();
     const config = createServerInfo();
     config.gerrit.instance_id = 'test-id';
-    element.getConfigModel().updateServerConfig(config);
+    configModel.updateServerConfig(config);
     assert.isTrue(loadPluginsStub.calledOnce);
     assert.isTrue(loadPluginsStub.calledWith([], 'test-id'));
   });
diff --git a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.ts b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.ts
index f554ff0..183425d 100644
--- a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.ts
+++ b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.ts
@@ -7,7 +7,6 @@
 import '../../shared/gr-button/gr-button';
 import '../../shared/gr-select/gr-select';
 import {EditPreferencesInfo} from '../../../types/common';
-import {getAppContext} from '../../../services/app-context';
 import {formStyles} from '../../../styles/gr-form-styles';
 import {menuPageStyles} from '../../../styles/gr-menu-page-styles';
 import {sharedStyles} from '../../../styles/shared-styles';
@@ -15,6 +14,8 @@
 import {customElement, query, state} from 'lit/decorators.js';
 import {convertToString} from '../../../utils/string-util';
 import {subscribe} from '../../lit/subscription-controller';
+import {resolve} from '../../../models/dependency';
+import {userModelToken} from '../../../models/user/user-model';
 
 @customElement('gr-edit-preferences')
 export class GrEditPreferences extends LitElement {
@@ -46,13 +47,13 @@
 
   @state() private originalEditPrefs?: EditPreferencesInfo;
 
-  private readonly userModel = getAppContext().userModel;
+  private readonly getUserModel = resolve(this, userModelToken);
 
   constructor() {
     super();
     subscribe(
       this,
-      () => this.userModel.editPreferences$,
+      () => this.getUserModel().editPreferences$,
       editPreferences => {
         this.originalEditPrefs = editPreferences;
         this.editPrefs = {...editPreferences};
@@ -307,7 +308,7 @@
 
   async save() {
     if (!this.editPrefs) return;
-    await this.userModel.updateEditPreference(this.editPrefs);
+    await this.getUserModel().updateEditPreference(this.editPrefs);
   }
 }
 
diff --git a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.ts b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.ts
index 460cc7c..9c23857 100644
--- a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.ts
+++ b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.ts
@@ -12,12 +12,13 @@
 import {state, customElement} from 'lit/decorators.js';
 import {BindValueChangeEvent} from '../../../types/events';
 import {subscribe} from '../../lit/subscription-controller';
-import {getAppContext} from '../../../services/app-context';
 import {deepEqual} from '../../../utils/deep-util';
 import {createDefaultPreferences} from '../../../constants/constants';
 import {fontStyles} from '../../../styles/gr-font-styles';
 import {classMap} from 'lit/directives/class-map.js';
 import {menuPageStyles} from '../../../styles/gr-menu-page-styles';
+import {userModelToken} from '../../../models/user/user-model';
+import {resolve} from '../../../models/dependency';
 
 @customElement('gr-menu-editor')
 export class GrMenuEditor extends LitElement {
@@ -33,13 +34,13 @@
   @state()
   newUrl = '';
 
-  private readonly userModel = getAppContext().userModel;
+  private readonly getUserModel = resolve(this, userModelToken);
 
   constructor() {
     super();
     subscribe(
       this,
-      () => this.userModel.preferences$,
+      () => this.getUserModel().preferences$,
       prefs => {
         this.originalPrefs = prefs;
         this.menuItems = [...prefs.my];
@@ -196,7 +197,7 @@
   }
 
   private handleSave() {
-    this.userModel.updatePreferences({
+    this.getUserModel().updatePreferences({
       ...this.originalPrefs,
       my: this.menuItems,
     });
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
index ff2904a..92b1f86 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
@@ -63,6 +63,7 @@
 import {resolve} from '../../../models/dependency';
 import {settingsViewModelToken} from '../../../models/views/settings';
 import {areNotificationsEnabled} from '../../../utils/worker-util';
+import {userModelToken} from '../../../models/user/user-model';
 
 const GERRIT_DOCS_BASE_URL =
   'https://gerrit-review.googlesource.com/' + 'Documentation';
@@ -201,7 +202,7 @@
 
   private readonly restApiService = getAppContext().restApiService;
 
-  private readonly userModel = getAppContext().userModel;
+  private readonly getUserModel = resolve(this, userModelToken);
 
   // private but used in test
   readonly flagsService = getAppContext().flagsService;
@@ -220,14 +221,14 @@
     );
     subscribe(
       this,
-      () => this.userModel.account$,
+      () => this.getUserModel().account$,
       acc => {
         this.account = acc;
       }
     );
     subscribe(
       this,
-      () => this.userModel.preferences$,
+      () => this.getUserModel().preferences$,
       prefs => {
         if (!prefs) {
           throw new Error('getPreferences returned undefined');
@@ -1148,7 +1149,7 @@
 
   // private but used in test
   handleSavePreferences() {
-    return this.userModel.updatePreferences(this.localPrefs);
+    return this.getUserModel().updatePreferences(this.localPrefs);
   }
 
   // private but used in test
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
index 9dfbc04..8c2c2db 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
@@ -74,6 +74,7 @@
 import {HtmlPatched} from '../../../utils/lit-util';
 import {createDiffUrl} from '../../../models/views/diff';
 import {createChangeUrl} from '../../../models/views/change';
+import {userModelToken} from '../../../models/user/user-model';
 
 declare global {
   interface HTMLElementEventMap {
@@ -247,12 +248,11 @@
   @state()
   saving = false;
 
-  // Private but used in tests.
-  readonly getCommentsModel = resolve(this, commentsModelToken);
+  private readonly getCommentsModel = resolve(this, commentsModelToken);
 
   private readonly getChangeModel = resolve(this, changeModelToken);
 
-  private readonly userModel = getAppContext().userModel;
+  private readonly getUserModel = resolve(this, userModelToken);
 
   private readonly reporting = getAppContext().reportingService;
 
@@ -281,7 +281,7 @@
     );
     subscribe(
       this,
-      () => this.userModel.account$,
+      () => this.getUserModel().account$,
       x => (this.account = x)
     );
     subscribe(
@@ -291,12 +291,12 @@
     );
     subscribe(
       this,
-      () => this.userModel.diffPreferences$,
+      () => this.getUserModel().diffPreferences$,
       x => this.syntaxLayer.setEnabled(!!x.syntax_highlighting)
     );
     subscribe(
       this,
-      () => this.userModel.preferences$,
+      () => this.getUserModel().preferences$,
       prefs => {
         const layers: DiffLayer[] = [this.syntaxLayer];
         if (!prefs.disable_token_highlighting) {
@@ -307,7 +307,7 @@
     );
     subscribe(
       this,
-      () => this.userModel.diffPreferences$,
+      () => this.getUserModel().diffPreferences$,
       prefs => {
         this.prefs = {
           ...prefs,
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.ts b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.ts
index 10e54c7..97eafc9 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.ts
@@ -30,6 +30,8 @@
 import {GrButton} from '../gr-button/gr-button';
 import {SpecialFilePath} from '../../../constants/constants';
 import {GrIcon} from '../gr-icon/gr-icon';
+import {commentsModelToken} from '../../../models/comments/comments-model';
+import {testResolver} from '../../../test/common-test-setup';
 
 const c1 = {
   author: {name: 'Kermit'},
@@ -311,7 +313,7 @@
     setup(async () => {
       savePromise = mockPromise<DraftInfo>();
       stub = sinon
-        .stub(element.getCommentsModel(), 'saveDraft')
+        .stub(testResolver(commentsModelToken), 'saveDraft')
         .returns(savePromise);
 
       element.thread = createThread(c1, {...c2, unresolved: true});
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
index 056dfe5..af82186 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
@@ -64,6 +64,7 @@
 import {KnownExperimentId} from '../../../services/flags/flags';
 import {isBase64FileContent} from '../../../api/rest-api';
 import {createDiffUrl} from '../../../models/views/diff';
+import {userModelToken} from '../../../models/user/user-model';
 
 const UNSAVED_MESSAGE = 'Unable to save draft';
 
@@ -229,10 +230,9 @@
 
   private readonly getChangeModel = resolve(this, changeModelToken);
 
-  // Private but used in tests.
-  readonly getCommentsModel = resolve(this, commentsModelToken);
+  private readonly getCommentsModel = resolve(this, commentsModelToken);
 
-  private readonly userModel = getAppContext().userModel;
+  private readonly getUserModel = resolve(this, userModelToken);
 
   private readonly shortcuts = new ShortcutController(this);
 
@@ -282,12 +282,12 @@
     }
     subscribe(
       this,
-      () => this.userModel.account$,
+      () => this.getUserModel().account$,
       x => (this.account = x)
     );
     subscribe(
       this,
-      () => this.userModel.isAdmin$,
+      () => this.getUserModel().isAdmin$,
       x => (this.isAdmin = x)
     );
 
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
index 1b48658..4e8c70f 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
@@ -47,9 +47,15 @@
 import {SinonStub} from 'sinon';
 import {fixture, html, assert} from '@open-wc/testing';
 import {GrButton} from '../gr-button/gr-button';
+import {testResolver} from '../../../test/common-test-setup';
+import {
+  CommentsModel,
+  commentsModelToken,
+} from '../../../models/comments/comments-model';
 
 suite('gr-comment tests', () => {
   let element: GrComment;
+  let commentsModel: CommentsModel;
   const account = {
     email: 'dhruvsri@google.com' as EmailAddress,
     name: 'Dhruv Srivastava',
@@ -77,6 +83,7 @@
         .comment=${comment}
       ></gr-comment>`
     );
+    commentsModel = testResolver(commentsModelToken);
   });
 
   suite('DOM rendering', () => {
@@ -548,9 +555,7 @@
 
     test('save', async () => {
       const savePromise = mockPromise<DraftInfo>();
-      const stub = sinon
-        .stub(element.getCommentsModel(), 'saveDraft')
-        .returns(savePromise);
+      const stub = sinon.stub(commentsModel, 'saveDraft').returns(savePromise);
 
       element.comment = createDraft();
       element.editing = true;
@@ -595,7 +600,7 @@
 
     test('save failed', async () => {
       sinon
-        .stub(element.getCommentsModel(), 'saveDraft')
+        .stub(commentsModel, 'saveDraft')
         .returns(Promise.reject(new Error('saving failed')));
 
       element.comment = createDraft();
@@ -615,7 +620,7 @@
     test('discard', async () => {
       const discardPromise = mockPromise<void>();
       const stub = sinon
-        .stub(element.getCommentsModel(), 'discardDraft')
+        .stub(commentsModel, 'discardDraft')
         .returns(discardPromise);
 
       element.comment = createDraft();
@@ -638,7 +643,7 @@
     });
 
     test('resolved comment state indicated by checkbox', async () => {
-      const saveStub = sinon.stub(element.getCommentsModel(), 'saveDraft');
+      const saveStub = sinon.stub(commentsModel, 'saveDraft');
       element.comment = {
         ...createComment(),
         __draft: true,
@@ -662,11 +667,8 @@
     });
 
     test('saving empty text calls discard()', async () => {
-      const saveStub = sinon.stub(element.getCommentsModel(), 'saveDraft');
-      const discardStub = sinon.stub(
-        element.getCommentsModel(),
-        'discardDraft'
-      );
+      const saveStub = sinon.stub(commentsModel, 'saveDraft');
+      const discardStub = sinon.stub(commentsModel, 'discardDraft');
       element.comment = createDraft();
       element.editing = true;
       await element.updateComplete;
@@ -740,9 +742,7 @@
     setup(async () => {
       clock = sinon.useFakeTimers();
       savePromise = mockPromise<DraftInfo>();
-      saveStub = sinon
-        .stub(element.getCommentsModel(), 'saveDraft')
-        .returns(savePromise);
+      saveStub = sinon.stub(commentsModel, 'saveDraft').returns(savePromise);
 
       element.comment = createUnsaved();
       element.editing = true;
diff --git a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.ts b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.ts
index 15d7072..019bec1 100644
--- a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.ts
+++ b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.ts
@@ -8,7 +8,6 @@
 import '../gr-button/gr-button';
 import '../gr-select/gr-select';
 import {DiffPreferencesInfo, IgnoreWhitespaceType} from '../../../types/diff';
-import {getAppContext} from '../../../services/app-context';
 import {subscribe} from '../../lit/subscription-controller';
 import {formStyles} from '../../../styles/gr-form-styles';
 import {sharedStyles} from '../../../styles/shared-styles';
@@ -18,6 +17,8 @@
 import {fire} from '../../../utils/event-util';
 import {ValueChangedEvent} from '../../../types/events';
 import {GrSelect} from '../gr-select/gr-select';
+import {resolve} from '../../../models/dependency';
+import {userModelToken} from '../../../models/user/user-model';
 
 @customElement('gr-diff-preferences')
 export class GrDiffPreferences extends LitElement {
@@ -51,13 +52,13 @@
 
   @state() private originalDiffPrefs?: DiffPreferencesInfo;
 
-  private readonly userModel = getAppContext().userModel;
+  private readonly getUserModel = resolve(this, userModelToken);
 
   constructor() {
     super();
     subscribe(
       this,
-      () => this.userModel.diffPreferences$,
+      () => this.getUserModel().diffPreferences$,
       diffPreferences => {
         if (!diffPreferences) return;
         this.originalDiffPrefs = diffPreferences;
@@ -314,7 +315,7 @@
 
   async save() {
     if (!this.diffPrefs) return;
-    await this.userModel.updateDiffPreference(this.diffPrefs);
+    await this.getUserModel().updateDiffPreference(this.diffPrefs);
     fire(this, 'has-unsaved-changes-changed', {
       value: this.hasUnsavedChanges(),
     });
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.ts b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.ts
index 2d93227..886894e 100644
--- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.ts
+++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.ts
@@ -16,6 +16,8 @@
 import {customElement, property, state} from 'lit/decorators.js';
 import {fire} from '../../../utils/event-util';
 import {BindValueChangeEvent} from '../../../types/events';
+import {resolve} from '../../../models/dependency';
+import {userModelToken} from '../../../models/user/user-model';
 
 declare global {
   interface HTMLElementEventMap {
@@ -53,7 +55,7 @@
   private readonly restApiService = getAppContext().restApiService;
 
   // Private but used in tests.
-  readonly userModel = getAppContext().userModel;
+  readonly getUserModel = resolve(this, userModelToken);
 
   private subscriptions: Subscription[] = [];
 
@@ -63,7 +65,7 @@
       this.loggedIn = loggedIn;
     });
     this.subscriptions.push(
-      this.userModel.preferences$.subscribe(prefs => {
+      this.getUserModel().preferences$.subscribe(prefs => {
         if (prefs?.download_scheme) {
           // Note (issue 5180): normalize the download scheme with lower-case.
           this.selectedScheme = prefs.download_scheme.toLowerCase();
@@ -194,7 +196,7 @@
       this.selectedScheme = scheme;
       fire(this, 'selected-scheme-changed', {value: scheme});
       if (this.loggedIn) {
-        this.userModel.updatePreferences({
+        this.getUserModel().updatePreferences({
           download_scheme: this.selectedScheme,
         });
       }
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.ts b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.ts
index 695c674..b1d4e36 100644
--- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.ts
@@ -18,6 +18,8 @@
 import {PaperTabsElement} from '@polymer/paper-tabs/paper-tabs';
 import {fixture, html, assert} from '@open-wc/testing';
 import {PaperTabElement} from '@polymer/paper-tabs/paper-tab';
+import {UserModel, userModelToken} from '../../../models/user/user-model';
+import {testResolver} from '../../../test/common-test-setup';
 
 suite('gr-download-commands', () => {
   let element: GrDownloadCommands;
@@ -170,11 +172,16 @@
     });
   });
   suite('authenticated', () => {
-    test('loads scheme from preferences', async () => {
-      const element: GrDownloadCommands = await fixture(
+    let element: GrDownloadCommands;
+    let userModel: UserModel;
+    setup(async () => {
+      userModel = testResolver(userModelToken);
+      element = await fixture(
         html`<gr-download-commands></gr-download-commands>`
       );
-      element.userModel.setPreferences({
+    });
+    test('loads scheme from preferences', async () => {
+      userModel.setPreferences({
         ...createPreferences(),
         download_scheme: 'repo',
       });
@@ -182,10 +189,7 @@
     });
 
     test('normalize scheme from preferences', async () => {
-      const element: GrDownloadCommands = await fixture(
-        html`<gr-download-commands></gr-download-commands>`
-      );
-      element.userModel.setPreferences({
+      userModel.setPreferences({
         ...createPreferences(),
         download_scheme: 'REPO',
       });
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents.ts b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents.ts
index 74dbec7..dc08648 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents.ts
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents.ts
@@ -45,6 +45,7 @@
 import {createSearchUrl} from '../../../models/views/search';
 import {createDashboardUrl} from '../../../models/views/dashboard';
 import {fire, fireEvent} from '../../../utils/event-util';
+import {userModelToken} from '../../../models/user/user-model';
 
 @customElement('gr-hovercard-account-contents')
 export class GrHovercardAccountContents extends LitElement {
@@ -77,8 +78,7 @@
 
   private readonly reporting = getAppContext().reportingService;
 
-  // private but used in tests
-  readonly userModel = getAppContext().userModel;
+  private readonly getUserModel = resolve(this, userModelToken);
 
   private readonly getConfigModel = resolve(this, configModelToken);
 
@@ -86,7 +86,7 @@
     super();
     subscribe(
       this,
-      () => this.userModel.account$,
+      () => this.getUserModel().account$,
       x => (this.selfAccount = x)
     );
     subscribe(
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents_test.ts b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents_test.ts
index 4f9eb6c..b217562 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents_test.ts
@@ -27,6 +27,8 @@
 } from '../../../test/test-data-generators';
 import {GrButton} from '../gr-button/gr-button';
 import {EventType} from '../../../types/events';
+import {testResolver} from '../../../test/common-test-setup';
+import {userModelToken} from '../../../models/user/user-model';
 
 suite('gr-hovercard-account-contents tests', () => {
   let element: GrHovercardAccountContents;
@@ -51,7 +53,7 @@
       html`<gr-hovercard-account-contents .account=${ACCOUNT} .change=${change}>
       </gr-hovercard-account-contents>`
     );
-    element.userModel.setAccount({...ACCOUNT});
+    testResolver(userModelToken).setAccount({...ACCOUNT});
     await element.updateComplete;
   });
 
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.ts b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.ts
index 4f3c65e..40e4c75 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.ts
@@ -15,7 +15,8 @@
 } from '../../../test/test-data-generators';
 import {GrButton} from '../gr-button/gr-button';
 import {GrHovercardAccountContents} from './gr-hovercard-account-contents';
-import {getAppContext} from '../../../services/app-context';
+import {userModelToken} from '../../../models/user/user-model';
+import {testResolver} from '../../../test/common-test-setup';
 
 suite('gr-hovercard-account tests', () => {
   let element: GrHovercardAccount;
@@ -33,7 +34,7 @@
       </gr-hovercard-account>`
     );
     await element.show({});
-    getAppContext().userModel.setAccount({...account});
+    testResolver(userModelToken).setAccount({...account});
     await element.updateComplete;
     contents = queryAndAssert(element, 'gr-hovercard-account-contents');
   });
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-mode-selector/gr-diff-mode-selector.ts b/polygerrit-ui/app/embed/diff/gr-diff-mode-selector/gr-diff-mode-selector.ts
index 5caffe6..5058ce8 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-mode-selector/gr-diff-mode-selector.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-mode-selector/gr-diff-mode-selector.ts
@@ -11,12 +11,12 @@
 import {customElement, property, state} from 'lit/decorators.js';
 import {IronA11yAnnouncer} from '@polymer/iron-a11y-announcer/iron-a11y-announcer';
 import {FixIronA11yAnnouncer} from '../../../types/types';
-import {getAppContext} from '../../../services/app-context';
 import {fireIronAnnounce} from '../../../utils/event-util';
 import {browserModelToken} from '../../../models/browser/browser-model';
 import {resolve} from '../../../models/dependency';
 import {css, html, LitElement} from 'lit';
 import {sharedStyles} from '../../../styles/shared-styles';
+import {userModelToken} from '../../../models/user/user-model';
 
 @customElement('gr-diff-mode-selector')
 export class GrDiffModeSelector extends LitElement {
@@ -34,7 +34,7 @@
 
   private readonly getBrowserModel = resolve(this, browserModelToken);
 
-  private readonly userModel = getAppContext().userModel;
+  private readonly getUserModel = resolve(this, userModelToken);
 
   private subscriptions: Subscription[] = [];
 
@@ -118,7 +118,7 @@
    */
   private setMode(newMode: DiffViewMode) {
     if (this.saveOnChange && this.mode && this.mode !== newMode) {
-      this.userModel.updatePreferences({diff_view: newMode});
+      this.getUserModel().updatePreferences({diff_view: newMode});
     }
     this.mode = newMode;
     let announcement;
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.ts
index 34af01e..d646988 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.ts
@@ -7,21 +7,17 @@
 import './gr-diff-mode-selector';
 import {GrDiffModeSelector} from './gr-diff-mode-selector';
 import {DiffViewMode} from '../../../constants/constants';
-import {
-  queryAndAssert,
-  stubUsers,
-  waitUntilObserved,
-} from '../../../test/test-utils';
+import {queryAndAssert, waitUntilObserved} from '../../../test/test-utils';
 import {fixture, html, assert} from '@open-wc/testing';
 import {wrapInProvider} from '../../../models/di-provider-element';
 import {
   BrowserModel,
   browserModelToken,
 } from '../../../models/browser/browser-model';
-import {getAppContext} from '../../../services/app-context';
-import {UserModel} from '../../../models/user/user-model';
+import {UserModel, userModelToken} from '../../../models/user/user-model';
 import {createPreferences} from '../../../test/test-data-generators';
 import {GrButton} from '../../../elements/shared/gr-button/gr-button';
+import {testResolver} from '../../../test/common-test-setup';
 
 suite('gr-diff-mode-selector tests', () => {
   let element: GrDiffModeSelector;
@@ -29,7 +25,7 @@
   let userModel: UserModel;
 
   setup(async () => {
-    userModel = getAppContext().userModel;
+    userModel = testResolver(userModelToken);
     browserModel = new BrowserModel(userModel);
     element = (
       await fixture(
@@ -129,7 +125,7 @@
 
   test('set mode', async () => {
     browserModel.setScreenWidth(0);
-    const saveStub = stubUsers('updatePreferences');
+    const saveStub = sinon.stub(userModel, 'updatePreferences');
 
     // Setting the mode initially does not save prefs.
     element.saveOnChange = true;
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 f865d6d..e762985 100644
--- a/polygerrit-ui/app/embed/gr-diff-app-context-init.ts
+++ b/polygerrit-ui/app/embed/gr-diff-app-context-init.ts
@@ -69,18 +69,12 @@
     storageService: (_ctx: Partial<AppContext>) => {
       throw new Error('storageService is not implemented');
     },
-    userModel: (_ctx: Partial<AppContext>) => {
-      throw new Error('userModel is not implemented');
-    },
     accountsModel: (_ctx: Partial<AppContext>) => {
       throw new Error('accountsModel is not implemented');
     },
     routerModel: (_ctx: Partial<AppContext>) => {
       throw new Error('routerModel is not implemented');
     },
-    shortcutsService: (_ctx: Partial<AppContext>) => {
-      throw new Error('shortcutsService is not implemented');
-    },
     pluginsModel: (_ctx: Partial<AppContext>) => {
       throw new Error('pluginsModel is not implemented');
     },
diff --git a/polygerrit-ui/app/models/change/change-model_test.ts b/polygerrit-ui/app/models/change/change-model_test.ts
index 4b51d5b..ce699dc 100644
--- a/polygerrit-ui/app/models/change/change-model_test.ts
+++ b/polygerrit-ui/app/models/change/change-model_test.ts
@@ -32,6 +32,8 @@
 import {ChangeState, LoadingStatus, updateChangeWithEdit} from './change-model';
 import {ChangeModel} from './change-model';
 import {assert} from '@open-wc/testing';
+import {testResolver} from '../../test/common-test-setup';
+import {userModelToken} from '../user/user-model';
 
 suite('updateChangeWithEdit() tests', () => {
   test('undefined change', async () => {
@@ -83,7 +85,7 @@
     changeModel = new ChangeModel(
       getAppContext().routerModel,
       getAppContext().restApiService,
-      getAppContext().userModel
+      testResolver(userModelToken)
     );
     knownChange = {
       ...createChange(),
diff --git a/polygerrit-ui/app/models/user/user-model.ts b/polygerrit-ui/app/models/user/user-model.ts
index ce142d7..97f90fa 100644
--- a/polygerrit-ui/app/models/user/user-model.ts
+++ b/polygerrit-ui/app/models/user/user-model.ts
@@ -24,6 +24,7 @@
 import {RestApiService} from '../../services/gr-rest-api/gr-rest-api';
 import {DiffPreferencesInfo} from '../../types/diff';
 import {select} from '../../utils/observable-util';
+import {define} from '../dependency';
 import {Model} from '../model';
 import {isDefined} from '../../types/types';
 
@@ -55,6 +56,8 @@
   capabilities?: AccountCapabilityInfo;
 }
 
+export const userModelToken = define<UserModel>('user-model');
+
 export class UserModel extends Model<UserState> {
   /**
    * Note that the initially emitted `undefined` value can mean "not loaded
diff --git a/polygerrit-ui/app/services/app-context-init.ts b/polygerrit-ui/app/services/app-context-init.ts
index fe47fd5..ee6ca0e 100644
--- a/polygerrit-ui/app/services/app-context-init.ts
+++ b/polygerrit-ui/app/services/app-context-init.ts
@@ -16,7 +16,7 @@
 import {ChecksModel, checksModelToken} from '../models/checks/checks-model';
 import {GrJsApiInterface} from '../elements/shared/gr-js-api-interface/gr-js-api-interface-element';
 import {GrStorageService} from './storage/gr-storage_impl';
-import {UserModel} from '../models/user/user-model';
+import {UserModel, userModelToken} from '../models/user/user-model';
 import {
   CommentsModel,
   commentsModelToken,
@@ -87,10 +87,6 @@
       return new GrJsApiInterface(reportingService);
     },
     storageService: (_ctx: Partial<AppContext>) => new GrStorageService(),
-    userModel: (ctx: Partial<AppContext>) => {
-      assertIsDefined(ctx.restApiService, 'restApiService');
-      return new UserModel(ctx.restApiService);
-    },
     accountsModel: (ctx: Partial<AppContext>) => {
       assertIsDefined(ctx.restApiService, 'restApiService');
       return new AccountsModel(ctx.restApiService);
@@ -116,7 +112,9 @@
   resolver: <T>(token: DependencyToken<T>) => T
 ): Map<DependencyToken<unknown>, Creator<unknown>> {
   const dependencies = new Map<DependencyToken<unknown>, Creator<unknown>>();
-  const browserModelCreator = () => new BrowserModel(appContext.userModel);
+  const userModelCreator = () => new UserModel(appContext.restApiService);
+  dependencies.set(userModelToken, userModelCreator);
+  const browserModelCreator = () => new BrowserModel(resolver(userModelToken));
   dependencies.set(browserModelToken, browserModelCreator);
 
   const adminViewModelCreator = () => new AdminViewModel();
@@ -140,8 +138,10 @@
   const repoViewModelCreator = () => new RepoViewModel();
   dependencies.set(repoViewModelToken, repoViewModelCreator);
   const searchViewModelCreator = () =>
-    new SearchViewModel(appContext.restApiService, appContext.userModel, () =>
-      resolver(navigationToken)
+    new SearchViewModel(
+      appContext.restApiService,
+      resolver(userModelToken),
+      () => resolver(navigationToken)
     );
   dependencies.set(searchViewModelToken, searchViewModelCreator);
   const settingsViewModelCreator = () => new SettingsViewModel();
@@ -172,7 +172,7 @@
     new ChangeModel(
       appContext.routerModel,
       appContext.restApiService,
-      appContext.userModel
+      resolver(userModelToken)
     );
   dependencies.set(changeModelToken, changeModelCreator);
 
@@ -210,7 +210,7 @@
   dependencies.set(checksModelToken, checksModelCreator);
 
   const shortcutServiceCreator = () =>
-    new ShortcutsService(appContext.userModel, appContext.reportingService);
+    new ShortcutsService(resolver(userModelToken), appContext.reportingService);
   dependencies.set(shortcutsServiceToken, shortcutServiceCreator);
 
   return dependencies;
diff --git a/polygerrit-ui/app/services/app-context.ts b/polygerrit-ui/app/services/app-context.ts
index 5f47c43..62e6331 100644
--- a/polygerrit-ui/app/services/app-context.ts
+++ b/polygerrit-ui/app/services/app-context.ts
@@ -11,7 +11,6 @@
 import {RestApiService} from './gr-rest-api/gr-rest-api';
 import {JsApiService} from '../elements/shared/gr-js-api-interface/gr-js-api-types';
 import {StorageService} from './storage/gr-storage';
-import {UserModel} from '../models/user/user-model';
 import {RouterModel} from './router/router-model';
 import {PluginsModel} from '../models/plugins/plugins-model';
 import {HighlightService} from './highlight/highlight-service';
@@ -26,7 +25,6 @@
   restApiService: RestApiService;
   jsApiService: JsApiService;
   storageService: StorageService;
-  userModel: UserModel;
   accountsModel: AccountsModel;
   pluginsModel: PluginsModel;
   highlightService: HighlightService;
diff --git a/polygerrit-ui/app/services/service-worker-installer_test.ts b/polygerrit-ui/app/services/service-worker-installer_test.ts
index d982dee..a036289 100644
--- a/polygerrit-ui/app/services/service-worker-installer_test.ts
+++ b/polygerrit-ui/app/services/service-worker-installer_test.ts
@@ -9,13 +9,15 @@
 import {assert} from '@open-wc/testing';
 import {createDefaultPreferences} from '../constants/constants';
 import {waitUntilObserved} from '../test/test-utils';
+import {testResolver} from '../test/common-test-setup';
+import {userModelToken} from '../models/user/user-model';
 
 suite('service worker installer tests', () => {
   test('init', async () => {
     const registerStub = sinon.stub(window.navigator.serviceWorker, 'register');
     const flagsService = getAppContext().flagsService;
     const reportingService = getAppContext().reportingService;
-    const userModel = getAppContext().userModel;
+    const userModel = testResolver(userModelToken);
     sinon.stub(flagsService, 'isEnabled').returns(true);
     new ServiceWorkerInstaller(flagsService, reportingService, userModel);
     const prefs = {
diff --git a/polygerrit-ui/app/services/shortcuts/shortcuts-service_test.ts b/polygerrit-ui/app/services/shortcuts/shortcuts-service_test.ts
index 5b38a8a..0a5e0a4 100644
--- a/polygerrit-ui/app/services/shortcuts/shortcuts-service_test.ts
+++ b/polygerrit-ui/app/services/shortcuts/shortcuts-service_test.ts
@@ -15,6 +15,8 @@
 import {getAppContext} from '../app-context';
 import {pressKey} from '../../test/test-utils';
 import {assert} from '@open-wc/testing';
+import {testResolver} from '../../test/common-test-setup';
+import {userModelToken} from '../../models/user/user-model';
 
 const KEY_A: Binding = {key: 'a'};
 
@@ -23,7 +25,7 @@
 
   setup(() => {
     service = new ShortcutsService(
-      getAppContext().userModel,
+      testResolver(userModelToken),
       getAppContext().reportingService
     );
   });
diff --git a/polygerrit-ui/app/test/test-app-context-init.ts b/polygerrit-ui/app/test/test-app-context-init.ts
index f0d28be..33ff997 100644
--- a/polygerrit-ui/app/test/test-app-context-init.ts
+++ b/polygerrit-ui/app/test/test-app-context-init.ts
@@ -19,8 +19,6 @@
 import {PluginsModel} from '../models/plugins/plugins-model';
 import {MockHighlightService} from '../services/highlight/highlight-service-mock';
 import {AccountsModel} from '../models/accounts-model/accounts-model';
-import {UserModel} from '../models/user/user-model';
-import {ShortcutsService} from '../services/shortcuts/shortcuts-service';
 import {createAppDependencies, Creator} from '../services/app-context-init';
 import {navigationToken} from '../elements/core/gr-navigation/gr-navigation';
 import {DependencyToken} from '../models/dependency';
@@ -42,20 +40,10 @@
       return new GrJsApiInterface(ctx.reportingService);
     },
     storageService: (_ctx: Partial<AppContext>) => grStorageMock,
-    userModel: (ctx: Partial<AppContext>) => {
-      assertIsDefined(ctx.restApiService, 'restApiService');
-      return new UserModel(ctx.restApiService);
-    },
     accountsModel: (ctx: Partial<AppContext>) => {
       assertIsDefined(ctx.restApiService, 'restApiService');
       return new AccountsModel(ctx.restApiService);
     },
-    shortcutsService: (ctx: Partial<AppContext>) => {
-      assertIsDefined(ctx.userModel, 'userModel');
-      assertIsDefined(ctx.flagsService, 'flagsService');
-      assertIsDefined(ctx.reportingService, 'reportingService');
-      return new ShortcutsService(ctx.userModel, ctx.reportingService);
-    },
     pluginsModel: (_ctx: Partial<AppContext>) => new PluginsModel(),
     highlightService: (ctx: Partial<AppContext>) => {
       assertIsDefined(ctx.reportingService, 'reportingService');
diff --git a/polygerrit-ui/app/test/test-utils.ts b/polygerrit-ui/app/test/test-utils.ts
index 6d45ad4..384ed68 100644
--- a/polygerrit-ui/app/test/test-utils.ts
+++ b/polygerrit-ui/app/test/test-utils.ts
@@ -12,7 +12,6 @@
 import {StorageService} from '../services/storage/gr-storage';
 import {AuthService} from '../services/gr-auth/gr-auth';
 import {ReportingService} from '../services/gr-reporting/gr-reporting';
-import {UserModel} from '../models/user/user-model';
 import {queryAndAssert, query} from '../utils/common-util';
 import {FlagsService} from '../services/flags/flags';
 import {Key, Modifier, whenVisible} from '../utils/dom-util';
@@ -109,10 +108,6 @@
   return sinon.spy(getAppContext().restApiService, method);
 }
 
-export function stubUsers<K extends keyof UserModel>(method: K) {
-  return sinon.stub(getAppContext().userModel, method);
-}
-
 export function stubHighlightService<K extends keyof HighlightService>(
   method: K
 ) {