Revert "Revert "Move reviewed files into model""
This reverts commit ba40029b1e8000224bc84720d4f6215a76a2d586.
Reason for revert: Integrate fix in Change 326058
Change-Id: Iff1409930a2b2732125a1a97fea38af291adcc37
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 1918972..441c233 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
@@ -221,7 +221,7 @@
_loggedIn = false;
@property({type: Array})
- _reviewed?: string[] = [];
+ reviewed?: string[] = [];
@property({type: Object, notify: true, observer: '_updateDiffPreferences'})
diffPrefs?: DiffPreferencesInfo;
@@ -318,6 +318,8 @@
private readonly userModel = getAppContext().userModel;
+ private readonly changeModel = getAppContext().changeModel;
+
private readonly getCommentsModel = resolve(this, commentsModelToken);
private readonly getBrowserModel = resolve(this, browserModelToken);
@@ -394,6 +396,9 @@
).subscribe(sizeBarInChangeTable => {
this._showSizeBars = sizeBarInChangeTable;
}),
+ this.changeModel.reviewedFiles$.subscribe(reviewedFiles => {
+ this.reviewed = reviewedFiles ?? [];
+ }),
];
getPluginLoader()
@@ -470,7 +475,7 @@
this._loading = true;
this.collapseAllDiffs();
- const promises = [];
+ const promises: Promise<boolean | void>[] = [];
promises.push(
this.restApiService
@@ -481,19 +486,7 @@
);
promises.push(
- this._getLoggedIn()
- .then(loggedIn => (this._loggedIn = loggedIn))
- .then(loggedIn => {
- if (!loggedIn) {
- return;
- }
-
- return this._getReviewedFiles(changeNum, patchRange).then(
- reviewed => {
- this._reviewed = reviewed;
- }
- );
- })
+ this._getLoggedIn().then(loggedIn => (this._loggedIn = loggedIn))
);
return Promise.all(promises).then(() => {
@@ -755,7 +748,7 @@
throw new Error('changeNum and patchRange must be set');
}
- return this.restApiService.saveFileReviewed(
+ return this.changeModel.setReviewedFilesStatus(
this.changeNum,
this.patchRange.patchNum,
path,
@@ -780,8 +773,7 @@
const paths = Object.keys(response).sort(specialFilePathCompare);
const files: NormalizedFileInfo[] = [];
for (let i = 0; i < paths.length; i++) {
- // TODO(TS): make copy instead of as NormalizedFileInfo
- const info = response[paths[i]] as NormalizedFileInfo;
+ const info = {...response[paths[i]]} as NormalizedFileInfo;
info.__path = paths[i];
info.lines_inserted = info.lines_inserted || 0;
info.lines_deleted = info.lines_deleted || 0;
@@ -1133,7 +1125,7 @@
'_filesByPath',
'changeComments',
'patchRange',
- '_reviewed',
+ 'reviewed',
'_loading'
)
_computeFiles(
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js
index e212bcb..bd12eae 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js
@@ -662,7 +662,7 @@
});
test('file review status', () => {
- element._reviewed = ['/COMMIT_MSG', 'myfile.txt'];
+ element.reviewed = ['/COMMIT_MSG', 'myfile.txt'];
element._filesByPath = {
'/COMMIT_MSG': {},
'file_added_in_rev2.txt': {},
@@ -1509,7 +1509,7 @@
size: 100,
},
};
- element._reviewed = ['/COMMIT_MSG', 'myfile.txt'];
+ element.reviewed = ['/COMMIT_MSG', 'myfile.txt'];
element._loggedIn = true;
element.changeNum = '42';
element.patchRange = {
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 459b696..2fdb650 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
@@ -112,8 +112,8 @@
import {addGlobalShortcut, Key, toggleClass} from '../../../utils/dom-util';
import {CursorMoveResult} from '../../../api/core';
import {isFalse, throttleWrap, until} from '../../../utils/async-util';
-import {filter, take} from 'rxjs/operators';
-import {Subscription, combineLatest} from 'rxjs';
+import {filter, take, switchMap} from 'rxjs/operators';
+import {combineLatest, Subscription} from 'rxjs';
import {listen} from '../../../services/shortcuts/shortcuts-service';
import {LoadingStatus} from '../../../services/change/change-model';
import {DisplayLine} from '../../../api/diff';
@@ -123,7 +123,6 @@
import {resolve, DIPolymerElement} from '../../../models/dependency';
import {BehaviorSubject} from 'rxjs';
-const ERR_REVIEW_STATUS = 'Couldn’t change file review status.';
const LOADING_BLAME = 'Loading blame...';
const LOADED_BLAME = 'Blame loaded';
@@ -273,20 +272,14 @@
@property({type: Object, computed: '_getRevisionInfo(_change)'})
_revisionInfo?: RevisionInfoObj;
- @property({type: Object})
- _reviewedFiles = new Set<string>();
-
@property({type: Number})
_focusLineNum?: number;
- private getReviewedParams: {
- changeNum?: NumericChangeId;
- patchNum?: PatchSetNum;
- } = {};
-
/** Called in disconnectedCallback. */
private cleanups: (() => void)[] = [];
+ private reviewedFiles = new Set<string>();
+
override keyboardShortcuts(): ShortcutListener[] {
return [
listen(Shortcut.LEFT_PANE, _ => this.cursor.moveLeft()),
@@ -422,29 +415,53 @@
})
);
+ this.subscriptions.push(
+ this.changeModel.reviewedFiles$.subscribe(reviewedFiles => {
+ this.reviewedFiles = new Set(reviewedFiles) ?? new Set();
+ })
+ );
+
+ this.subscriptions.push(
+ this.changeModel.diffPath$.subscribe(path => (this._path = path))
+ );
+
+ this.subscriptions.push(
+ combineLatest(
+ this.changeModel.diffPath$,
+ this.changeModel.reviewedFiles$
+ ).subscribe(([path, files]) => {
+ this.$.reviewed.checked = !!path && !!files && files.includes(path);
+ })
+ );
+
// When user initially loads the diff view, we want to autmatically mark
// the file as reviewed if they have it enabled. We can't observe these
// properties since the method will be called anytime a property updates
// but we only want to call this on the initial load.
this.subscriptions.push(
- combineLatest([
- this.changeModel.currentPatchNum$,
- this.routerModel.routerView$,
- this.changeModel.diffPath$,
- this.userModel.diffPreferences$,
- ])
+ this.changeModel.diffPath$
.pipe(
- filter(
- ([currentPatchNum, routerView, path, diffPrefs]) =>
- !!currentPatchNum &&
- routerView === GerritView.DIFF &&
- !!path &&
- !!diffPrefs
- ),
- take(1)
+ filter(diffPath => !!diffPath),
+ switchMap(() =>
+ combineLatest(
+ this.changeModel.currentPatchNum$,
+ this.routerModel.routerView$,
+ this.userModel.diffPreferences$,
+ this.changeModel.reviewedFiles$
+ ).pipe(
+ filter(
+ ([currentPatchNum, routerView, diffPrefs, reviewedFiles]) =>
+ !!currentPatchNum &&
+ routerView === GerritView.DIFF &&
+ !!diffPrefs &&
+ !!reviewedFiles
+ ),
+ take(1)
+ )
+ )
)
- .subscribe(([currentPatchNum, _routerView, path, diffPrefs]) => {
- this.setReviewedStatus(currentPatchNum!, path!, diffPrefs);
+ .subscribe(([currentPatchNum, _routerView, diffPrefs]) => {
+ this.setReviewedStatus(currentPatchNum!, diffPrefs);
})
);
this.subscriptions.push(
@@ -479,17 +496,18 @@
super.disconnectedCallback();
}
+ /**
+ * Set initial review status of the file.
+ * automatically mark the file as reviewed if manual review is not set.
+ */
+
async setReviewedStatus(
currentPatchNum: PatchSetNum,
- path: string,
diffPrefs: DiffPreferencesInfo
) {
const loggedIn = await this._getLoggedIn();
if (!loggedIn) return;
- await this._getReviewedFiles();
- if (diffPrefs.manual_review) {
- this.$.reviewed.checked = this._getReviewedStatus(path!);
- } else {
+ if (!diffPrefs.manual_review) {
this._setReviewed(true, currentPatchNum as RevisionPatchSetNum);
}
}
@@ -590,32 +608,14 @@
patchNum: RevisionPatchSetNum | undefined = this._patchRange?.patchNum
) {
if (this._editMode) return;
- this.$.reviewed.checked = reviewed;
- if (!patchNum || !this._path) return;
+ if (!patchNum || !this._path || !this._changeNum) return;
const path = this._path;
// if file is already reviewed then do not make a saveReview request
- if (this._reviewedFiles.has(path) && reviewed) return;
- if (reviewed) this._reviewedFiles.add(path);
- else this._reviewedFiles.delete(path);
- this._saveReviewedState(reviewed, patchNum).catch(err => {
- if (this._reviewedFiles.has(path)) this._reviewedFiles.delete(path);
- else this._reviewedFiles.add(path);
- fireAlert(this, ERR_REVIEW_STATUS);
- throw err;
- });
- }
-
- _saveReviewedState(
- reviewed: boolean,
- patchNum?: RevisionPatchSetNum
- ): Promise<Response | undefined> {
- if (!this._changeNum) return Promise.resolve(undefined);
- if (!patchNum) return Promise.resolve(undefined);
- if (!this._path) return Promise.resolve(undefined);
- return this.restApiService.saveFileReviewed(
+ if (this.reviewedFiles.has(path) && reviewed) return;
+ this.changeModel.setReviewedFilesStatus(
this._changeNum,
patchNum,
- this._path,
+ path,
reviewed
);
}
@@ -736,11 +736,11 @@
private navigateToUnreviewedFile(direction: string) {
if (!this._path) return;
if (!this._fileList) return;
- if (!this._reviewedFiles) return;
+ if (!this.reviewedFiles) return;
// Ensure that the currently viewed file always appears in unreviewedFiles
// so we resolve the right "next" file.
const unreviewedFiles = this._fileList.filter(
- file => file === this._path || !this._reviewedFiles.has(file)
+ file => file === this._path || !this.reviewedFiles.has(file)
);
this._navToFile(this._path, unreviewedFiles, direction === 'next' ? 1 : -1);
@@ -940,30 +940,6 @@
return {path: fileList[idx]};
}
- _getReviewedFiles(changeNum?: NumericChangeId, patchNum?: PatchSetNum) {
- if (!changeNum || !patchNum) return;
- if (
- this.getReviewedParams.changeNum === changeNum &&
- this.getReviewedParams.patchNum === patchNum
- ) {
- return Promise.resolve();
- }
- this.getReviewedParams = {
- changeNum,
- patchNum,
- };
- return this.restApiService
- .getReviewedFiles(changeNum, patchNum)
- .then(files => {
- this._reviewedFiles = new Set(files);
- });
- }
-
- _getReviewedStatus(path: string) {
- if (this._editMode) return false;
- return this._reviewedFiles.has(path);
- }
-
_initLineOfInterestAndCursor(leftSide: boolean) {
this.$.diffHost.lineOfInterest = this._getLineOfInterest(leftSide);
this._initCursor(leftSide);
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 b590625..77bbded 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
@@ -19,7 +19,7 @@
import './gr-diff-view.js';
import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
import {ChangeStatus, DiffViewMode, createDefaultDiffPrefs} from '../../../constants/constants.js';
-import {stubRestApi, stubUsers, waitUntil} from '../../../test/test-utils.js';
+import {stubRestApi, stubUsers, waitUntil, stubChange} from '../../../test/test-utils.js';
import {ChangeComments} from '../gr-comment-api/gr-comment-api.js';
import {GerritView} from '../../../services/router/router-model.js';
import {
@@ -1190,10 +1190,10 @@
test('_prefs.manual_review true means set reviewed is not ' +
'automatically called', async () => {
- const saveReviewedStub = sinon.stub(element, '_saveReviewedState')
+ const setReviewedFileStatusStub = stubChange('setReviewedFilesStatus')
.callsFake(() => Promise.resolve());
- const getReviewedStub = sinon.stub(element, '_getReviewedStatus')
- .returns(false);
+
+ const setReviewedStatusStub = sinon.spy(element, 'setReviewedStatus');
sinon.stub(element.$.diffHost, 'reload');
sinon.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
@@ -1204,7 +1204,9 @@
element.userModel.setDiffPreferences(diffPreferences);
element.changeModel.setState({
change: createChange(),
- diffPath: '/COMMIT_MSG'});
+ diffPath: '/COMMIT_MSG',
+ reviewedFiles: [],
+ });
element.routerModel.setState({
changeNum: TEST_NUMERIC_CHANGE_ID, view: GerritView.DIFF, patchNum: 2}
@@ -1214,25 +1216,21 @@
basePatchNum: 1,
};
- await waitUntil(() => getReviewedStub.called);
+ await waitUntil(() => setReviewedStatusStub.called);
- assert.isFalse(saveReviewedStub.called);
- assert.isTrue(getReviewedStub.called);
+ assert.isFalse(setReviewedFileStatusStub.called);
// if prefs are updated then the reviewed status should not be set again
element.userModel.setDiffPreferences(createDefaultDiffPrefs());
await flush();
- assert.isFalse(saveReviewedStub.called);
- assert.isTrue(getReviewedStub.calledOnce);
+ assert.isFalse(setReviewedFileStatusStub.called);
});
test('_prefs.manual_review false means set reviewed is called',
async () => {
- const saveReviewedStub = sinon.stub(element, '_saveReviewedState')
+ const setReviewedFileStatusStub = stubChange('setReviewedFilesStatus')
.callsFake(() => Promise.resolve());
- const getReviewedStub = sinon.stub(element, '_getReviewedStatus')
- .returns(false);
sinon.stub(element.$.diffHost, 'reload');
sinon.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
@@ -1243,7 +1241,9 @@
element.userModel.setDiffPreferences(diffPreferences);
element.changeModel.setState({
change: createChange(),
- diffPath: '/COMMIT_MSG'});
+ diffPath: '/COMMIT_MSG',
+ reviewedFiles: [],
+ });
element.routerModel.setState({
changeNum: TEST_NUMERIC_CHANGE_ID, view: GerritView.DIFF,
@@ -1254,22 +1254,23 @@
basePatchNum: 1,
};
- await waitUntil(() => saveReviewedStub.called);
+ await waitUntil(() => setReviewedFileStatusStub.called);
- assert.isTrue(saveReviewedStub.called);
- assert.isFalse(getReviewedStub.called);
+ assert.isTrue(setReviewedFileStatusStub.called);
});
test('file review status', async () => {
+ element.changeModel.setState({
+ change: createChange(),
+ diffPath: '/COMMIT_MSG',
+ reviewedFiles: [],
+ });
sinon.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
- const saveReviewedStub = sinon.stub(element, '_saveReviewedState')
+ const saveReviewedStub = stubChange('setReviewedFilesStatus')
.callsFake(() => Promise.resolve());
sinon.stub(element.$.diffHost, 'reload');
element.userModel.setDiffPreferences(createDefaultDiffPrefs());
- element.changeModel.setState({
- change: createChange(),
- diffPath: '/COMMIT_MSG'});
element.routerModel.setState({
changeNum: TEST_NUMERIC_CHANGE_ID, view: GerritView.DIFF, patchNum: 2}
@@ -1282,19 +1283,28 @@
await waitUntil(() => saveReviewedStub.called);
+ element.changeModel.updateStateFileReviewed('/COMMIT_MSG', true);
+ await flush();
+
const reviewedStatusCheckBox = element.root.querySelector(
'input[type="checkbox"]');
assert.isTrue(reviewedStatusCheckBox.checked);
- assert.deepEqual(saveReviewedStub.lastCall.args, [true, 2]);
+ assert.deepEqual(saveReviewedStub.lastCall.args,
+ ['42', 2, '/COMMIT_MSG', true]);
MockInteractions.tap(reviewedStatusCheckBox);
assert.isFalse(reviewedStatusCheckBox.checked);
- assert.deepEqual(saveReviewedStub.lastCall.args, [false, 2]);
+ assert.deepEqual(saveReviewedStub.lastCall.args,
+ ['42', 2, '/COMMIT_MSG', false]);
+
+ element.changeModel.updateStateFileReviewed('/COMMIT_MSG', false);
+ await flush();
MockInteractions.tap(reviewedStatusCheckBox);
assert.isTrue(reviewedStatusCheckBox.checked);
- assert.deepEqual(saveReviewedStub.lastCall.args, [true, 2]);
+ assert.deepEqual(saveReviewedStub.lastCall.args,
+ ['42', 2, '/COMMIT_MSG', true]);
const callCount = saveReviewedStub.callCount;
@@ -1307,7 +1317,7 @@
});
test('file review status with edit loaded', () => {
- const saveReviewedStub = sinon.stub(element, '_saveReviewedState');
+ const saveReviewedStub = stubChange('setReviewedFilesStatus');
element._patchRange = {patchNum: EditPatchSetNum};
flush();
@@ -1796,7 +1806,7 @@
isAtEndStub.returns(true);
element._files = getFilesFromFileList(['file1', 'file2', 'file3']);
- element._reviewedFiles = new Set(['file2']);
+ element.reviewedFiles = new Set(['file2']);
element._path = 'file1';
nowStub.returns(5);
@@ -1840,7 +1850,7 @@
isAtStartStub.returns(true);
element._files = getFilesFromFileList(['file1', 'file2', 'file3']);
- element._reviewedFiles = new Set(['file2']);
+ element.reviewedFiles = new Set(['file2']);
element._path = 'file3';
nowStub.returns(5);
@@ -1887,7 +1897,7 @@
test('shift+m navigates to next unreviewed file', () => {
element._files = getFilesFromFileList(['file1', 'file2', 'file3']);
- element._reviewedFiles = new Set(['file1', 'file2']);
+ element.reviewedFiles = new Set(['file1', 'file2']);
element._path = 'file1';
const reviewedStub = sinon.stub(element, '_setReviewed');
const navStub = sinon.stub(element, '_navToFile');
diff --git a/polygerrit-ui/app/services/change/change-model.ts b/polygerrit-ui/app/services/change/change-model.ts
index 0ba1e6c..9634147 100644
--- a/polygerrit-ui/app/services/change/change-model.ts
+++ b/polygerrit-ui/app/services/change/change-model.ts
@@ -28,6 +28,7 @@
Observable,
Subscription,
forkJoin,
+ of,
} from 'rxjs';
import {
map,
@@ -43,6 +44,7 @@
computeLatestPatchNum,
} from '../../utils/patch-set-util';
import {ParsedChangeInfo} from '../../types/types';
+import {fireAlert} from '../../utils/event-util';
import {ChangeInfo} from '../../types/common';
import {RestApiService} from '../gr-rest-api/gr-rest-api';
@@ -58,6 +60,8 @@
LOADED = 'LOADED',
}
+const ERR_REVIEW_STATUS = 'Couldn’t change file review status.';
+
export interface ChangeState {
/**
* If `change` is undefined, this must be either NOT_LOADED or LOADING.
@@ -71,6 +75,12 @@
* Does not apply to change-view or edit-view.
*/
diffPath?: string;
+ /**
+ * The list of reviewed files, kept in the model because we want changes made
+ * in one view to reflect on other views without re-rendering the other views.
+ * Undefined means it's still loading and empty set means no files reviewed.
+ */
+ reviewedFiles?: string[];
}
/**
@@ -112,6 +122,10 @@
};
export class ChangeModel extends Model<ChangeState> implements Finalizable {
+ private change?: ParsedChangeInfo;
+
+ private currentPatchNum?: PatchSetNum;
+
public readonly change$ = select(
this.state$,
changeState => changeState.change
@@ -127,6 +141,11 @@
changeState => changeState?.diffPath
);
+ public readonly reviewedFiles$ = select(
+ this.state$,
+ changeState => changeState?.reviewedFiles
+ );
+
public readonly changeNum$ = select(this.change$, change => change?._number);
public readonly repo$ = select(this.change$, change => change?.project);
@@ -205,6 +224,21 @@
// helps with that.
this.updateStateChange(change ?? undefined);
}),
+ this.change$.subscribe(change => (this.change = change)),
+ this.currentPatchNum$.subscribe(
+ currentPatchNum => (this.currentPatchNum = currentPatchNum)
+ ),
+ combineLatest([this.currentPatchNum$, this.changeNum$])
+ .pipe(
+ switchMap(([currentPatchNum, changeNum]) => {
+ if (!changeNum || !currentPatchNum) {
+ this.updateStateReviewedFiles([]);
+ return of(undefined);
+ }
+ return from(this.fetchReviewedFiles(currentPatchNum!, changeNum!));
+ })
+ )
+ .subscribe(),
];
}
@@ -221,6 +255,71 @@
this.setState({...current, diffPath});
}
+ updateStateReviewedFiles(reviewedFiles: string[]) {
+ const current = this.subject$.getValue();
+ this.setState({...current, reviewedFiles});
+ }
+
+ updateStateFileReviewed(file: string, reviewed: boolean) {
+ const current = this.subject$.getValue();
+ if (current.reviewedFiles === undefined) {
+ // Reviewed files haven't loaded yet.
+ // TODO(dhruvsri): disable updating status if reviewed files are not loaded.
+ fireAlert(
+ document,
+ 'Updating status failed. Reviewed files not loaded yet.'
+ );
+ return;
+ }
+ const reviewedFiles = [...current.reviewedFiles];
+
+ // File is already reviewed and is being marked reviewed
+ if (reviewedFiles.includes(file) && reviewed) return;
+ // File is not reviewed and is being marked not reviewed
+ if (!reviewedFiles.includes(file) && !reviewed) return;
+
+ if (reviewed) reviewedFiles.push(file);
+ else reviewedFiles.splice(reviewedFiles.indexOf(file), 1);
+ this.setState({...current, reviewedFiles});
+ }
+
+ fetchReviewedFiles(currentPatchNum: PatchSetNum, changeNum: NumericChangeId) {
+ return this.restApiService.getLoggedIn().then(loggedIn => {
+ if (!loggedIn) return;
+ this.restApiService
+ .getReviewedFiles(changeNum, currentPatchNum)
+ .then(files => {
+ if (
+ changeNum !== this.change?._number ||
+ currentPatchNum !== this.currentPatchNum
+ )
+ return;
+ this.updateStateReviewedFiles(files ?? []);
+ });
+ });
+ }
+
+ setReviewedFilesStatus(
+ changeNum: NumericChangeId,
+ patchNum: PatchSetNum,
+ file: string,
+ reviewed: boolean
+ ) {
+ return this.restApiService
+ .saveFileReviewed(changeNum, patchNum, file, reviewed)
+ .then(() => {
+ if (
+ changeNum !== this.change?._number ||
+ patchNum !== this.currentPatchNum
+ )
+ return;
+ this.updateStateFileReviewed(file, reviewed);
+ })
+ .catch(() => {
+ fireAlert(document, ERR_REVIEW_STATUS);
+ });
+ }
+
/**
* Typically you would just subscribe to change$ yourself to get updates. But
* sometimes it is nice to also be able to get the current ChangeInfo on
diff --git a/polygerrit-ui/app/test/test-utils.ts b/polygerrit-ui/app/test/test-utils.ts
index fbc2433..88167e0 100644
--- a/polygerrit-ui/app/test/test-utils.ts
+++ b/polygerrit-ui/app/test/test-utils.ts
@@ -28,6 +28,7 @@
import {queryAndAssert, query} from '../utils/common-util';
import {FlagsService} from '../services/flags/flags';
import {Key, Modifier} from '../utils/dom-util';
+import {ChangeModel} from '../services/change/change-model';
export {query, queryAll, queryAndAssert} from '../utils/common-util';
export interface MockPromise<T> extends Promise<T> {
@@ -111,6 +112,10 @@
return sinon.spy(getAppContext().restApiService, method);
}
+export function stubChange<K extends keyof ChangeModel>(method: K) {
+ return sinon.stub(getAppContext().changeModel, method);
+}
+
export function stubUsers<K extends keyof UserModel>(method: K) {
return sinon.stub(getAppContext().userModel, method);
}