Move loading of related changes into a new model

Release-Notes: skip
Google-Bug-Id: b/247042673
Change-Id: Ib46587cb011dac9905f375f77fe9aa2302f815d9
diff --git a/polygerrit-ui/app/models/change/related-changes-model_test.ts b/polygerrit-ui/app/models/change/related-changes-model_test.ts
new file mode 100644
index 0000000..31c0026
--- /dev/null
+++ b/polygerrit-ui/app/models/change/related-changes-model_test.ts
@@ -0,0 +1,238 @@
+/**
+ * @license
+ * Copyright 2023 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../test/common-test-setup';
+import {getAppContext} from '../../services/app-context';
+import {ChangeModel, changeModelToken} from '../change/change-model';
+import {assert} from '@open-wc/testing';
+import {testResolver} from '../../test/common-test-setup';
+import {RelatedChangesModel} from './related-changes-model';
+import {configModelToken} from '../config/config-model';
+import {SinonStub} from 'sinon';
+import {
+  ChangeInfo,
+  RelatedChangesInfo,
+  SubmittedTogetherInfo,
+} from '../../types/common';
+import {stubRestApi, waitUntilObserved} from '../../test/test-utils';
+import {
+  createParsedChange,
+  createRelatedChangesInfo,
+  createRelatedChangeAndCommitInfo,
+  createChange,
+} from '../../test/test-data-generators';
+import {ChangeStatus, TopicName} from '../../api/rest-api';
+
+suite('related-changes-model tests', () => {
+  let model: RelatedChangesModel;
+  let changeModel: ChangeModel;
+
+  setup(async () => {
+    changeModel = testResolver(changeModelToken);
+    model = new RelatedChangesModel(
+      changeModel,
+      testResolver(configModelToken),
+      getAppContext().restApiService
+    );
+    await waitUntilObserved(changeModel.change$, c => c === undefined);
+  });
+
+  teardown(() => {
+    model.finalize();
+  });
+
+  test('register and fetch', async () => {
+    assert.equal('', '');
+  });
+
+  suite('related changes and hasParent', async () => {
+    let getRelatedChangesStub: SinonStub;
+    let getRelatedChangesResponse: RelatedChangesInfo;
+    let hasParent: boolean | undefined;
+
+    setup(() => {
+      getRelatedChangesStub = stubRestApi('getRelatedChanges').callsFake(() =>
+        Promise.resolve(getRelatedChangesResponse)
+      );
+      model.hasParent$.subscribe(x => (hasParent = x));
+    });
+
+    test('relatedChanges initially undefined', async () => {
+      await waitUntilObserved(
+        model.relatedChanges$,
+        relatedChanges => relatedChanges === undefined
+      );
+      assert.isFalse(getRelatedChangesStub.called);
+      assert.isUndefined(hasParent);
+    });
+
+    test('relatedChanges loading empty', async () => {
+      changeModel.updateStateChange({...createParsedChange()});
+
+      await waitUntilObserved(
+        model.relatedChanges$,
+        relatedChanges => relatedChanges?.length === 0
+      );
+      assert.isTrue(getRelatedChangesStub.calledOnce);
+      assert.isFalse(hasParent);
+    });
+
+    test('relatedChanges loading one change', async () => {
+      getRelatedChangesResponse = {
+        ...createRelatedChangesInfo(),
+        changes: [createRelatedChangeAndCommitInfo()],
+      };
+      changeModel.updateStateChange({...createParsedChange()});
+
+      await waitUntilObserved(
+        model.relatedChanges$,
+        relatedChanges => relatedChanges?.length === 1
+      );
+      assert.isTrue(getRelatedChangesStub.calledOnce);
+      assert.isTrue(hasParent);
+    });
+  });
+
+  suite('loadSubmittedTogether', async () => {
+    let getChangesSubmittedTogetherStub: SinonStub;
+    let getChangesSubmittedTogetherResponse: SubmittedTogetherInfo;
+
+    setup(() => {
+      getChangesSubmittedTogetherStub = stubRestApi(
+        'getChangesSubmittedTogether'
+      ).callsFake(() => Promise.resolve(getChangesSubmittedTogetherResponse));
+    });
+
+    test('submittedTogether initially undefined', async () => {
+      await waitUntilObserved(
+        model.submittedTogether$,
+        submittedTogether => submittedTogether === undefined
+      );
+      assert.isFalse(getChangesSubmittedTogetherStub.called);
+    });
+
+    test('submittedTogether emits', async () => {
+      getChangesSubmittedTogetherResponse = {
+        changes: [createChange()],
+        non_visible_changes: 0,
+      };
+      changeModel.updateStateChange({...createParsedChange()});
+
+      await waitUntilObserved(
+        model.submittedTogether$,
+        submittedTogether => submittedTogether?.changes?.length === 1
+      );
+      assert.isTrue(getChangesSubmittedTogetherStub.calledOnce);
+    });
+  });
+
+  suite('loadCherryPicks', async () => {
+    let getChangeCherryPicksStub: SinonStub;
+    let getChangeCherryPicksResponse: ChangeInfo[];
+
+    setup(() => {
+      getChangeCherryPicksStub = stubRestApi('getChangeCherryPicks').callsFake(
+        () => Promise.resolve(getChangeCherryPicksResponse)
+      );
+    });
+
+    test('cherryPicks initially undefined', async () => {
+      await waitUntilObserved(
+        model.cherryPicks$,
+        cherryPicks => cherryPicks === undefined
+      );
+      assert.isFalse(getChangeCherryPicksStub.called);
+    });
+
+    test('cherryPicks emits', async () => {
+      getChangeCherryPicksResponse = [createChange()];
+      changeModel.updateStateChange({...createParsedChange()});
+
+      await waitUntilObserved(
+        model.cherryPicks$,
+        cherryPicks => cherryPicks?.length === 1
+      );
+      assert.isTrue(getChangeCherryPicksStub.calledOnce);
+    });
+  });
+
+  suite('loadConflictingChanges', async () => {
+    let getChangeConflictsStub: SinonStub;
+    let getChangeConflictsResponse: ChangeInfo[];
+
+    setup(() => {
+      getChangeConflictsStub = stubRestApi('getChangeConflicts').callsFake(() =>
+        Promise.resolve(getChangeConflictsResponse)
+      );
+    });
+
+    test('conflictingChanges initially undefined', async () => {
+      await waitUntilObserved(
+        model.conflictingChanges$,
+        conflictingChanges => conflictingChanges === undefined
+      );
+      assert.isFalse(getChangeConflictsStub.called);
+    });
+
+    test('conflictingChanges not loaded for merged changes', async () => {
+      getChangeConflictsResponse = [createChange()];
+      changeModel.updateStateChange({
+        ...createParsedChange(),
+        mergeable: true,
+        status: ChangeStatus.MERGED,
+      });
+
+      await waitUntilObserved(
+        model.conflictingChanges$,
+        conflictingChanges => conflictingChanges === undefined
+      );
+      assert.isFalse(getChangeConflictsStub.called);
+    });
+
+    test('conflictingChanges emits', async () => {
+      getChangeConflictsResponse = [createChange()];
+      changeModel.updateStateChange({...createParsedChange(), mergeable: true});
+
+      await waitUntilObserved(
+        model.conflictingChanges$,
+        conflictingChanges => conflictingChanges?.length === 1
+      );
+      assert.isTrue(getChangeConflictsStub.calledOnce);
+    });
+  });
+
+  suite('loadSameTopicChanges', async () => {
+    let getChangesWithSameTopicStub: SinonStub;
+    let getChangesWithSameTopicResponse: ChangeInfo[];
+
+    setup(() => {
+      getChangesWithSameTopicStub = stubRestApi(
+        'getChangesWithSameTopic'
+      ).callsFake(() => Promise.resolve(getChangesWithSameTopicResponse));
+    });
+
+    test('sameTopicChanges initially undefined', async () => {
+      await waitUntilObserved(
+        model.sameTopicChanges$,
+        sameTopicChanges => sameTopicChanges === undefined
+      );
+      assert.isFalse(getChangesWithSameTopicStub.called);
+    });
+
+    test('sameTopicChanges emits', async () => {
+      getChangesWithSameTopicResponse = [createChange()];
+      changeModel.updateStateChange({
+        ...createParsedChange(),
+        topic: 'test-topic' as TopicName,
+      });
+
+      await waitUntilObserved(
+        model.sameTopicChanges$,
+        sameTopicChanges => sameTopicChanges?.length === 1
+      );
+      assert.isTrue(getChangesWithSameTopicStub.calledOnce);
+    });
+  });
+});