Make commets-service responsible for loading comments based on state

Instead of change-view and diff-view calling `reloadAll` the service is
now subscribing to state changes and will update the comments
independently of the lifecycles of the view components.

Change-Id: I3a0694e51ae4b5b0226ff655ecf524036a3e544d
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 2a4290a..8a28cb1 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
@@ -2021,23 +2021,6 @@
       });
   }
 
-  /**
-   * Fetches a new changeComment object, and data for all types of comments
-   * (comments, robot comments, draft comments) is requested.
-   */
-  _reloadComments() {
-    // We are resetting all comment related properties, because we want to avoid
-    // a new change being loaded and then paired with outdated comments.
-    this._changeComments = undefined;
-    this._commentThreads = undefined;
-    this._draftCommentThreads = undefined;
-    this._robotCommentThreads = undefined;
-    if (!this._changeNum)
-      throw new Error('missing required changeNum property');
-
-    this.commentsService.reloadAll(this._changeNum, this._patchRange?.patchNum);
-  }
-
   @observe('_changeComments')
   changeCommentsChanged(comments?: ChangeComments) {
     if (!comments) return;
@@ -2119,8 +2102,6 @@
     });
     allDataPromises.push(projectConfigLoaded);
 
-    this._reloadComments();
-
     let coreDataPromise;
 
     // If the patch number is specified
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
index 6facdca..8d75230 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
@@ -20,7 +20,6 @@
 
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {createDefaultDiffPrefs, Side} from '../../../constants/constants.js';
-import {_testOnly_resetState} from '../../../services/comments/comments-model.js';
 import {createChange, createComment, createCommentThread} from '../../../test/test-data-generators.js';
 import {addListenerForTest, mockPromise, stubRestApi} from '../../../test/test-utils.js';
 import {EditPatchSetNum, ParentPatchSetNum} from '../../../types/common.js';
@@ -43,7 +42,6 @@
     element.path = 'some/path';
     sinon.stub(element.reporting, 'time');
     sinon.stub(element.reporting, 'timeEnd');
-    _testOnly_resetState();
     await flush();
   });
 
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 2d47ce3..64186f4 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
@@ -347,8 +347,6 @@
 
   private readonly userService = appContext.userService;
 
-  private readonly commentsService = appContext.commentsService;
-
   private readonly shortcuts = appContext.shortcutsService;
 
   _throttledToggleFileReviewed?: (e: KeyboardEvent) => void;
@@ -1073,8 +1071,6 @@
 
     if (!this._change) promises.push(this._getChangeDetail(this._changeNum));
 
-    if (!this._changeComments) this._loadComments(value.patchNum);
-
     promises.push(this._getChangeEdit());
 
     this.$.diffHost.cancel();
@@ -1463,11 +1459,6 @@
     return url;
   }
 
-  _loadComments(patchSet?: PatchSetNum) {
-    assertIsDefined(this._changeNum, '_changeNum');
-    return this.commentsService.reloadAll(this._changeNum, patchSet);
-  }
-
   @observe(
     '_changeComments',
     '_files.changeFilesByPath',
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js
index 46ec5d0..5c04ab0 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js
@@ -30,7 +30,7 @@
 import {EditPatchSetNum} from '../../../types/common.js';
 import {CursorMoveResult} from '../../../api/core.js';
 import {EventType} from '../../../types/events.js';
-import {_testOnly_resetState, _testOnly_setState} from '../../../services/browser/browser-model.js';
+import {_testOnly_setState} from '../../../services/browser/browser-model.js';
 
 const basicFixture = fixtureFromElement('gr-diff-view');
 
@@ -69,8 +69,6 @@
       stubRestApi('getDiffDrafts').returns(Promise.resolve({}));
       stubRestApi('getPortedComments').returns(Promise.resolve({}));
 
-      _testOnly_resetState();
-
       element = basicFixture.instantiate();
       element._changeNum = '42';
       element._path = 'some/path.txt';
@@ -2040,7 +2038,6 @@
           Promise.resolve([]));
       element = basicFixture.instantiate();
       element._changeNum = '42';
-      return element._loadComments();
     });
 
     test('_getFiles add files with comments without changes', () => {
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 595de34..9e0027a 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
@@ -50,7 +50,6 @@
   stubReporting,
   stubRestApi,
 } from '../../../test/test-utils';
-import {_testOnly_resetState} from '../../../services/comments/comments-model';
 import {SinonStub} from 'sinon';
 
 const basicFixture = fixtureFromElement('gr-comment-thread');
@@ -63,7 +62,6 @@
 
     setup(() => {
       stubRestApi('getLoggedIn').returns(Promise.resolve(false));
-      _testOnly_resetState();
       element = basicFixture.instantiate();
       element.patchNum = 3 as PatchSetNum;
       element.changeNum = 1 as NumericChangeId;
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 00edc07..09ac95b 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
@@ -384,11 +384,7 @@
 
     test('delete comment', async () => {
       const stub = stubRestApi('deleteComment').returns(
-        Promise.resolve({
-          id: '1' as UrlEncodedCommentId,
-          updated: '1' as Timestamp,
-          ...createComment(),
-        })
+        Promise.resolve(createComment())
       );
       const openSpy = sinon.spy(element.confirmDeleteOverlay!, 'open');
       element.changeNum = 42 as NumericChangeId;
@@ -1183,6 +1179,7 @@
 
     test('draft prevent save when disabled', async () => {
       const saveStub = sinon.stub(element, 'save').returns(Promise.resolve());
+      sinon.stub(element, '_fireEdit');
       element.showActions = true;
       element.draft = true;
       await flush();
diff --git a/polygerrit-ui/app/services/browser/browser-model.ts b/polygerrit-ui/app/services/browser/browser-model.ts
index db790f6..8cb6575 100644
--- a/polygerrit-ui/app/services/browser/browser-model.ts
+++ b/polygerrit-ui/app/services/browser/browser-model.ts
@@ -33,11 +33,13 @@
 
 const initialState: BrowserState = {};
 
-// Mutable for testing
-let privateState$ = new BehaviorSubject(initialState);
+const privateState$ = new BehaviorSubject(initialState);
 
 export function _testOnly_resetState() {
-  privateState$ = new BehaviorSubject(initialState);
+  // We cannot assign a new subject to privateState$, because all the selectors
+  // have already subscribed to the original subject. So we have to emit the
+  // initial state on the existing subject.
+  privateState$.next({...initialState});
 }
 
 export function _testOnly_setState(state: BrowserState) {
diff --git a/polygerrit-ui/app/services/change/change-model.ts b/polygerrit-ui/app/services/change/change-model.ts
index 962ef4d..7df0c22 100644
--- a/polygerrit-ui/app/services/change/change-model.ts
+++ b/polygerrit-ui/app/services/change/change-model.ts
@@ -40,6 +40,13 @@
 
 const privateState$ = new BehaviorSubject(initialState);
 
+export function _testOnly_resetState() {
+  // We cannot assign a new subject to privateState$, because all the selectors
+  // have already subscribed to the original subject. So we have to emit the
+  // initial state on the existing subject.
+  privateState$.next({...initialState});
+}
+
 // Re-exporting as Observable so that you can only subscribe, but not emit.
 export const changeState$: Observable<ChangeState> = privateState$;
 
diff --git a/polygerrit-ui/app/services/checks/checks-model.ts b/polygerrit-ui/app/services/checks/checks-model.ts
index 75c24b6d..6435252 100644
--- a/polygerrit-ui/app/services/checks/checks-model.ts
+++ b/polygerrit-ui/app/services/checks/checks-model.ts
@@ -126,11 +126,13 @@
   pluginStateSelected: {},
 };
 
-// Mutable for testing
-let privateState$ = new BehaviorSubject(initialState);
+const privateState$ = new BehaviorSubject(initialState);
 
 export function _testOnly_resetState() {
-  privateState$ = new BehaviorSubject(initialState);
+  // We cannot assign a new subject to privateState$, because all the selectors
+  // have already subscribed to the original subject. So we have to emit the
+  // initial state on the existing subject.
+  privateState$.next({...initialState});
 }
 
 export function _testOnly_setState(state: ChecksState) {
diff --git a/polygerrit-ui/app/services/checks/checks-model_test.ts b/polygerrit-ui/app/services/checks/checks-model_test.ts
index dbd3f86..0be0451 100644
--- a/polygerrit-ui/app/services/checks/checks-model_test.ts
+++ b/polygerrit-ui/app/services/checks/checks-model_test.ts
@@ -18,7 +18,6 @@
 import './checks-model';
 import {
   _testOnly_getState,
-  _testOnly_resetState,
   ChecksPatchset,
   updateStateSetLoading,
   updateStateSetProvider,
@@ -52,7 +51,6 @@
 
 suite('checks-model tests', () => {
   test('updateStateSetProvider', () => {
-    _testOnly_resetState();
     updateStateSetProvider(PLUGIN_NAME, ChecksPatchset.LATEST);
     assert.deepEqual(current(), {
       pluginName: PLUGIN_NAME,
@@ -65,7 +63,6 @@
   });
 
   test('loading and first time load', () => {
-    _testOnly_resetState();
     updateStateSetProvider(PLUGIN_NAME, ChecksPatchset.LATEST);
     assert.isFalse(current().loading);
     assert.isTrue(current().firstTimeLoad);
@@ -84,14 +81,12 @@
   });
 
   test('updateStateSetResults', () => {
-    _testOnly_resetState();
     updateStateSetResults(PLUGIN_NAME, RUNS, [], [], ChecksPatchset.LATEST);
     assert.lengthOf(current().runs, 1);
     assert.lengthOf(current().runs[0].results!, 1);
   });
 
   test('updateStateUpdateResult', () => {
-    _testOnly_resetState();
     updateStateSetResults(PLUGIN_NAME, RUNS, [], [], ChecksPatchset.LATEST);
     assert.equal(
       current().runs[0].results![0].summary,
diff --git a/polygerrit-ui/app/services/comments/comments-model.ts b/polygerrit-ui/app/services/comments/comments-model.ts
index 850acbc..ef57416 100644
--- a/polygerrit-ui/app/services/comments/comments-model.ts
+++ b/polygerrit-ui/app/services/comments/comments-model.ts
@@ -51,7 +51,10 @@
 const privateState$ = new BehaviorSubject(initialState);
 
 export function _testOnly_resetState() {
-  privateState$.next(initialState);
+  // We cannot assign a new subject to privateState$, because all the selectors
+  // have already subscribed to the original subject. So we have to emit the
+  // initial state on the existing subject.
+  privateState$.next({...initialState});
 }
 
 // Re-exporting as Observable so that you can only subscribe, but not emit.
@@ -65,11 +68,21 @@
   privateState$.next(state);
 }
 
+export const comments$ = commentState$.pipe(
+  map(commentState => commentState.comments),
+  distinctUntilChanged()
+);
+
 export const drafts$ = commentState$.pipe(
   map(commentState => commentState.drafts),
   distinctUntilChanged()
 );
 
+export const portedComments$ = commentState$.pipe(
+  map(commentState => commentState.portedComments),
+  distinctUntilChanged()
+);
+
 export const discardedDrafts$ = commentState$.pipe(
   map(commentState => commentState.discardedDrafts),
   distinctUntilChanged()
@@ -95,6 +108,11 @@
   privateState$.next(state);
 }
 
+/** Called when the change number changes. Wipes out all data from the state. */
+export function updateStateReset() {
+  publishState({...initialState});
+}
+
 export function updateStateComments(comments?: {
   [path: string]: CommentInfo[];
 }) {
diff --git a/polygerrit-ui/app/services/comments/comments-model_test.ts b/polygerrit-ui/app/services/comments/comments-model_test.ts
index e389254..30fc7cf 100644
--- a/polygerrit-ui/app/services/comments/comments-model_test.ts
+++ b/polygerrit-ui/app/services/comments/comments-model_test.ts
@@ -22,13 +22,11 @@
 import {
   updateStateDeleteDraft,
   _testOnly_getState,
-  _testOnly_resetState,
   _testOnly_setState,
 } from './comments-model';
 
 suite('comments model tests', () => {
   test('updateStateDeleteDraft', () => {
-    _testOnly_resetState();
     const draft = createDraft();
     draft.id = '1' as UrlEncodedCommentId;
     _testOnly_setState({
diff --git a/polygerrit-ui/app/services/comments/comments-service.ts b/polygerrit-ui/app/services/comments/comments-service.ts
index 2e28ece..5896b52 100644
--- a/polygerrit-ui/app/services/comments/comments-service.ts
+++ b/polygerrit-ui/app/services/comments/comments-service.ts
@@ -31,7 +31,10 @@
   updateStatePortedDrafts,
   updateStateUndoDiscardedDraft,
   discardedDrafts$,
+  updateStateReset,
 } from './comments-model';
+import {changeNum$, currentPatchNum$} from '../change/change-model';
+import {combineLatest} from 'rxjs';
 
 export class CommentsService {
   private discardedDrafts?: UIDraft[] = [];
@@ -40,21 +43,20 @@
     discardedDrafts$.subscribe(
       discardedDrafts => (this.discardedDrafts = discardedDrafts)
     );
-  }
-
-  /**
-   * Load all comments (with drafts and robot comments) for the given change
-   * number. The returned promise resolves when the comments have loaded, but
-   * does not yield the comment data.
-   */
-  // TODO(dhruvsri): listen to changeNum changes or reload event to update
-  // automatically
-  reloadAll(changeNum: NumericChangeId, patchNum = CURRENT as RevisionId) {
-    this.reloadComments(changeNum);
-    this.reloadRobotComments(changeNum);
-    this.reloadDrafts(changeNum);
-    this.reloadPortedComments(changeNum, patchNum);
-    this.reloadPortedDrafts(changeNum, patchNum);
+    changeNum$.subscribe(changeNum => {
+      updateStateReset();
+      if (!changeNum) return;
+      this.reloadComments(changeNum);
+      this.reloadRobotComments(changeNum);
+      this.reloadDrafts(changeNum);
+    });
+    combineLatest([changeNum$, currentPatchNum$]).subscribe(
+      ([changeNum, currentPatchNum]) => {
+        if (!changeNum || !currentPatchNum) return;
+        this.reloadPortedComments(changeNum, currentPatchNum);
+        this.reloadPortedDrafts(changeNum, currentPatchNum);
+      }
+    );
   }
 
   reloadComments(changeNum: NumericChangeId): Promise<void> {
diff --git a/polygerrit-ui/app/services/comments/comments-service_test.ts b/polygerrit-ui/app/services/comments/comments-service_test.ts
index 0e6e042..a35768e 100644
--- a/polygerrit-ui/app/services/comments/comments-service_test.ts
+++ b/polygerrit-ui/app/services/comments/comments-service_test.ts
@@ -18,109 +18,63 @@
 import '../../test/common-test-setup-karma';
 import {
   createComment,
-  createFixSuggestionInfo,
+  createParsedChange,
+  TEST_NUMERIC_CHANGE_ID,
 } from '../../test/test-data-generators';
-import {stubRestApi} from '../../test/test-utils';
-import {
-  NumericChangeId,
-  RobotId,
-  RobotRunId,
-  Timestamp,
-  UrlEncodedCommentId,
-} from '../../types/common';
+import {stubRestApi, waitUntil, waitUntilCalled} from '../../test/test-utils';
 import {appContext} from '../app-context';
 import {CommentsService} from './comments-service';
+import {updateState as updateChangeState} from '../change/change-model';
+import {
+  GerritView,
+  updateState as updateRouterState,
+} from '../router/router-model';
+import {comments$, portedComments$} from './comments-model';
+import {PathToCommentsInfoMap} from '../../types/common';
 
 suite('change service tests', () => {
-  let commentsService: CommentsService;
-
-  test('loads logged-out', () => {
-    const changeNum = 1234 as NumericChangeId;
-    commentsService = new CommentsService(appContext.restApiService);
-    stubRestApi('getLoggedIn').returns(Promise.resolve(false));
+  test('loads comments', async () => {
+    new CommentsService(appContext.restApiService);
     const diffCommentsSpy = stubRestApi('getDiffComments').returns(
-      Promise.resolve({
-        'foo.c': [
-          {
-            ...createComment(),
-            id: '123' as UrlEncodedCommentId,
-            message: 'Done',
-            updated: '2017-02-08 16:40:49' as Timestamp,
-          },
-        ],
-      })
+      Promise.resolve({'foo.c': [createComment()]})
     );
     const diffRobotCommentsSpy = stubRestApi('getDiffRobotComments').returns(
-      Promise.resolve({
-        'foo.c': [
-          {
-            ...createComment(),
-            id: '321' as UrlEncodedCommentId,
-            message: 'Done',
-            updated: '2017-02-08 16:40:49' as Timestamp,
-            robot_id: 'robot_1' as RobotId,
-            robot_run_id: 'run_1' as RobotRunId,
-            properties: {},
-            fix_suggestions: [
-              createFixSuggestionInfo('fix_1'),
-              createFixSuggestionInfo('fix_2'),
-            ],
-          },
-        ],
-      })
+      Promise.resolve({})
     );
     const diffDraftsSpy = stubRestApi('getDiffDrafts').returns(
       Promise.resolve({})
     );
-
-    commentsService.reloadAll(changeNum);
-    assert.isTrue(diffCommentsSpy.calledWithExactly(changeNum));
-    assert.isTrue(diffRobotCommentsSpy.calledWithExactly(changeNum));
-    assert.isTrue(diffDraftsSpy.calledWithExactly(changeNum));
-  });
-
-  test('loads logged-in', () => {
-    const changeNum = 1234 as NumericChangeId;
-
-    stubRestApi('getLoggedIn').returns(Promise.resolve(true));
-    const diffCommentsSpy = stubRestApi('getDiffComments').returns(
-      Promise.resolve({
-        'foo.c': [
-          {
-            ...createComment(),
-            id: '123' as UrlEncodedCommentId,
-            message: 'Done',
-            updated: '2017-02-08 16:40:49' as Timestamp,
-          },
-        ],
-      })
+    const portedCommentsSpy = stubRestApi('getPortedComments').returns(
+      Promise.resolve({'foo.c': [createComment()]})
     );
-    const diffRobotCommentsSpy = stubRestApi('getDiffRobotComments').returns(
-      Promise.resolve({
-        'foo.c': [
-          {
-            ...createComment(),
-            id: '321' as UrlEncodedCommentId,
-            message: 'Done',
-            updated: '2017-02-08 16:40:49' as Timestamp,
-            robot_id: 'robot_1' as RobotId,
-            robot_run_id: 'run_1' as RobotRunId,
-            properties: {},
-            fix_suggestions: [
-              createFixSuggestionInfo('fix_1'),
-              createFixSuggestionInfo('fix_2'),
-            ],
-          },
-        ],
-      })
-    );
-    const diffDraftsSpy = stubRestApi('getDiffDrafts').returns(
+    const portedDraftsSpy = stubRestApi('getPortedDrafts').returns(
       Promise.resolve({})
     );
+    let comments: PathToCommentsInfoMap = {};
+    comments$.subscribe(c => (comments = c));
+    let portedComments: PathToCommentsInfoMap = {};
+    portedComments$.subscribe(c => (portedComments = c));
 
-    commentsService.reloadAll(changeNum);
-    assert.isTrue(diffCommentsSpy.calledWithExactly(changeNum));
-    assert.isTrue(diffRobotCommentsSpy.calledWithExactly(changeNum));
-    assert.isTrue(diffDraftsSpy.calledWithExactly(changeNum));
+    updateRouterState(GerritView.CHANGE, TEST_NUMERIC_CHANGE_ID);
+    updateChangeState(createParsedChange());
+
+    await waitUntilCalled(diffCommentsSpy, 'diffCommentsSpy');
+    await waitUntilCalled(diffRobotCommentsSpy, 'diffRobotCommentsSpy');
+    await waitUntilCalled(diffDraftsSpy, 'diffDraftsSpy');
+    await waitUntilCalled(portedCommentsSpy, 'portedCommentsSpy');
+    await waitUntilCalled(portedDraftsSpy, 'portedDraftsSpy');
+    await waitUntil(
+      () => Object.keys(comments).length > 0,
+      'comment in model not set'
+    );
+    await waitUntil(
+      () => Object.keys(portedComments).length > 0,
+      'ported comment in model not set'
+    );
+
+    assert.equal(comments['foo.c'].length, 1);
+    assert.equal(comments['foo.c'][0].id, '12345');
+    assert.equal(portedComments['foo.c'].length, 1);
+    assert.equal(portedComments['foo.c'][0].id, '12345');
   });
 });
diff --git a/polygerrit-ui/app/services/router/router-model.ts b/polygerrit-ui/app/services/router/router-model.ts
index b3cdf9e..ae3d848 100644
--- a/polygerrit-ui/app/services/router/router-model.ts
+++ b/polygerrit-ui/app/services/router/router-model.ts
@@ -47,6 +47,13 @@
 
 const privateState$ = new BehaviorSubject<RouterState>(initialState);
 
+export function _testOnly_resetState() {
+  // We cannot assign a new subject to privateState$, because all the selectors
+  // have already subscribed to the original subject. So we have to emit the
+  // initial state on the existing subject.
+  privateState$.next({...initialState});
+}
+
 // Re-exporting as Observable so that you can only subscribe, but not emit.
 export const routerState$: Observable<RouterState> = privateState$;
 
diff --git a/polygerrit-ui/app/services/user/user-model.ts b/polygerrit-ui/app/services/user/user-model.ts
index 000887c..df307d6 100644
--- a/polygerrit-ui/app/services/user/user-model.ts
+++ b/polygerrit-ui/app/services/user/user-model.ts
@@ -37,11 +37,13 @@
   diffPreferences: createDefaultDiffPrefs(),
 };
 
-// Mutable for testing
-let privateState$ = new BehaviorSubject(initialState);
+const privateState$ = new BehaviorSubject(initialState);
 
 export function _testOnly_resetState() {
-  privateState$ = new BehaviorSubject(initialState);
+  // We cannot assign a new subject to privateState$, because all the selectors
+  // have already subscribed to the original subject. So we have to emit the
+  // initial state on the existing subject.
+  privateState$.next({...initialState});
 }
 
 export function _testOnly_setState(state: UserState) {
diff --git a/polygerrit-ui/app/test/common-test-setup.ts b/polygerrit-ui/app/test/common-test-setup.ts
index bd5504a..05adb41 100644
--- a/polygerrit-ui/app/test/common-test-setup.ts
+++ b/polygerrit-ui/app/test/common-test-setup.ts
@@ -45,6 +45,12 @@
 import {updatePreferences} from '../services/user/user-model';
 import {createDefaultPreferences} from '../constants/constants';
 import {appContext} from '../services/app-context';
+import {_testOnly_resetState as resetBrowserState} from '../services/browser/browser-model';
+import {_testOnly_resetState as resetChangeState} from '../services/change/change-model';
+import {_testOnly_resetState as resetChecksState} from '../services/checks/checks-model';
+import {_testOnly_resetState as resetCommentsState} from '../services/comments/comments-model';
+import {_testOnly_resetState as resetRouterState} from '../services/router/router-model';
+import {_testOnly_resetState as resetUserState} from '../services/user/user-model';
 
 declare global {
   interface Window {
@@ -106,6 +112,14 @@
   // tests.
   initGlobalVariables();
   _testOnly_initGerritPluginApi();
+
+  resetBrowserState();
+  resetChangeState();
+  resetChecksState();
+  resetCommentsState();
+  resetRouterState();
+  resetUserState();
+
   const shortcuts = appContext.shortcutsService;
   assert.isTrue(shortcuts._testOnly_isEmpty());
   const selection = document.getSelection();
diff --git a/polygerrit-ui/app/test/test-data-generators.ts b/polygerrit-ui/app/test/test-data-generators.ts
index dd56ce2..91cd2f3 100644
--- a/polygerrit-ui/app/test/test-data-generators.ts
+++ b/polygerrit-ui/app/test/test-data-generators.ts
@@ -95,7 +95,12 @@
 import {AppElementChangeViewParams} from '../elements/gr-app-types';
 import {CommitInfoWithRequiredCommit} from '../elements/change/gr-change-metadata/gr-change-metadata';
 import {WebLinkInfo} from '../types/diff';
-import {createCommentThreads, UIComment, UIDraft} from '../utils/comment-util';
+import {
+  createCommentThreads,
+  UIComment,
+  UIDraft,
+  UIHuman,
+} from '../utils/comment-util';
 import {GerritView} from '../services/router/router-model';
 import {ChangeComments} from '../elements/diff/gr-comment-api/gr-comment-api';
 import {EditRevisionInfo, ParsedChangeInfo} from '../types/types';
@@ -488,7 +493,7 @@
   };
 }
 
-export function createComment(): UIComment {
+export function createComment(): UIHuman {
   return {
     patch_set: 1 as PatchSetNum,
     id: '12345' as UrlEncodedCommentId,
diff --git a/polygerrit-ui/app/test/test-utils.ts b/polygerrit-ui/app/test/test-utils.ts
index 4a513f8..63c125e 100644
--- a/polygerrit-ui/app/test/test-utils.ts
+++ b/polygerrit-ui/app/test/test-utils.ts
@@ -19,7 +19,7 @@
 import {_testOnly_resetEndpoints} from '../elements/shared/gr-js-api-interface/gr-plugin-endpoints';
 import {appContext} from '../services/app-context';
 import {RestApiService} from '../services/gr-rest-api/gr-rest-api';
-import {SinonSpy} from 'sinon';
+import {SinonSpy, SinonStub} from 'sinon';
 import {StorageService} from '../services/storage/gr-storage';
 import {AuthService} from '../services/gr-auth/gr-auth';
 import {ReportingService} from '../services/gr-reporting/gr-reporting';
@@ -185,6 +185,7 @@
 ): Promise<void> {
   const start = Date.now();
   let sleep = 0;
+  if (predicate()) return Promise.resolve();
   return new Promise((resolve, reject) => {
     const waiter = () => {
       if (predicate()) {
@@ -200,6 +201,10 @@
   });
 }
 
+export function waitUntilCalled(stub: SinonStub, name: string) {
+  return waitUntil(() => stub.called, `${name} was not called`);
+}
+
 /**
  * Promisify an event callback to simplify async...await tests.
  *