blob: 5347b41bff3cbdb9625ab6cd41d16b9855c56718 [file] [log] [blame]
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {
createAccountWithIdNameAndEmail,
createChange,
createRevisions,
} from '../../test/test-data-generators';
import {
ChangeInfo,
NumericChangeId,
ChangeStatus,
HttpMethod,
SubmitRequirementStatus,
AccountInfo,
ReviewerState,
AccountId,
} from '../../api/rest-api';
import {BulkActionsModel, LoadingState} from './bulk-actions-model';
import {getAppContext} from '../../services/app-context';
import '../../test/common-test-setup-karma';
import {stubRestApi, waitUntilObserved} from '../../test/test-utils';
import {mockPromise} from '../../test/test-utils';
import {SinonStubbedMember} from 'sinon';
import {RestApiService} from '../../services/gr-rest-api/gr-rest-api';
import {ReviewInput} from '../../types/common';
suite('bulk actions model test', () => {
let bulkActionsModel: BulkActionsModel;
setup(() => {
bulkActionsModel = new BulkActionsModel(getAppContext().restApiService);
});
test('does not request detailed changes when no changes are synced', () => {
const detailedActionsStub = stubRestApi('getDetailedChangesWithActions');
bulkActionsModel.sync([]);
assert.isTrue(detailedActionsStub.notCalled);
});
test('add changes before sync', () => {
const c1 = createChange();
c1._number = 1 as NumericChangeId;
const c2 = createChange();
c2._number = 2 as NumericChangeId;
assert.isEmpty(bulkActionsModel.getState().selectedChangeNums);
assert.throws(() => bulkActionsModel.addSelectedChangeNum(c1._number));
assert.isEmpty(bulkActionsModel.getState().selectedChangeNums);
bulkActionsModel.sync([c1, c2]);
bulkActionsModel.addSelectedChangeNum(c2._number);
assert.sameMembers(bulkActionsModel.getState().selectedChangeNums, [
c2._number,
]);
bulkActionsModel.removeSelectedChangeNum(c2._number);
assert.isEmpty(bulkActionsModel.getState().selectedChangeNums);
});
test('add and remove selected changes', () => {
const c1 = createChange();
c1._number = 1 as NumericChangeId;
const c2 = createChange();
c2._number = 2 as NumericChangeId;
bulkActionsModel.sync([c1, c2]);
assert.isEmpty(bulkActionsModel.getState().selectedChangeNums);
bulkActionsModel.addSelectedChangeNum(c1._number);
assert.sameMembers(bulkActionsModel.getState().selectedChangeNums, [
c1._number,
]);
bulkActionsModel.addSelectedChangeNum(c2._number);
assert.sameMembers(bulkActionsModel.getState().selectedChangeNums, [
c1._number,
c2._number,
]);
bulkActionsModel.removeSelectedChangeNum(c1._number);
assert.sameMembers(bulkActionsModel.getState().selectedChangeNums, [
c2._number,
]);
bulkActionsModel.removeSelectedChangeNum(c2._number);
assert.isEmpty(bulkActionsModel.getState().selectedChangeNums);
});
test('clears selected change numbers', async () => {
const c1 = createChange();
c1._number = 1 as NumericChangeId;
const c2 = createChange();
c2._number = 2 as NumericChangeId;
bulkActionsModel.sync([c1, c2]);
bulkActionsModel.addSelectedChangeNum(c1._number);
bulkActionsModel.addSelectedChangeNum(c2._number);
let selectedChangeNums = await waitUntilObserved(
bulkActionsModel!.selectedChangeNums$,
s => s.length === 2
);
let totalChangeCount = await waitUntilObserved(
bulkActionsModel.totalChangeCount$,
totalChangeCount => totalChangeCount === 2
);
assert.sameMembers(selectedChangeNums, [c1._number, c2._number]);
assert.equal(totalChangeCount, 2);
bulkActionsModel.clearSelectedChangeNums();
selectedChangeNums = await waitUntilObserved(
bulkActionsModel!.selectedChangeNums$,
s => s.length === 0
);
totalChangeCount = await waitUntilObserved(
bulkActionsModel.totalChangeCount$,
totalChangeCount => totalChangeCount === 2
);
assert.isEmpty(selectedChangeNums);
assert.equal(totalChangeCount, 2);
});
test('selects all changes', async () => {
const c1 = createChange();
c1._number = 1 as NumericChangeId;
const c2 = createChange();
c2._number = 2 as NumericChangeId;
bulkActionsModel.sync([c1, c2]);
let selectedChangeNums = await waitUntilObserved(
bulkActionsModel!.selectedChangeNums$,
s => s.length === 0
);
let totalChangeCount = await waitUntilObserved(
bulkActionsModel.totalChangeCount$,
totalChangeCount => totalChangeCount === 2
);
assert.isEmpty(selectedChangeNums);
assert.equal(totalChangeCount, 2);
bulkActionsModel.selectAll();
selectedChangeNums = await waitUntilObserved(
bulkActionsModel!.selectedChangeNums$,
s => s.length === 2
);
totalChangeCount = await waitUntilObserved(
bulkActionsModel.totalChangeCount$,
totalChangeCount => totalChangeCount === 2
);
assert.sameMembers(selectedChangeNums, [c1._number, c2._number]);
assert.equal(totalChangeCount, 2);
});
suite('abandon changes', () => {
let detailedActionsStub: SinonStubbedMember<
RestApiService['getDetailedChangesWithActions']
>;
setup(async () => {
detailedActionsStub = stubRestApi('getDetailedChangesWithActions');
const c1 = createChange();
c1._number = 1 as NumericChangeId;
const c2 = createChange();
c2._number = 2 as NumericChangeId;
detailedActionsStub.returns(
Promise.resolve([
{...c1, actions: {abandon: {method: HttpMethod.POST}}},
{...c2, status: ChangeStatus.ABANDONED},
])
);
bulkActionsModel.sync([c1, c2]);
bulkActionsModel.addSelectedChangeNum(c1._number);
bulkActionsModel.addSelectedChangeNum(c2._number);
});
test('already abandoned change does not call executeChangeAction', () => {
const actionStub = stubRestApi('executeChangeAction');
bulkActionsModel.abandonChanges();
assert.equal(actionStub.callCount, 1);
assert.deepEqual(actionStub.lastCall.args.slice(0, 5), [
1 as NumericChangeId,
HttpMethod.POST,
'/abandon',
undefined,
{message: ''},
]);
});
});
suite('add reviewers', () => {
const accounts: AccountInfo[] = [
createAccountWithIdNameAndEmail(0),
createAccountWithIdNameAndEmail(1),
];
const changes: ChangeInfo[] = [
{
...createChange(),
_number: 1 as NumericChangeId,
subject: 'Subject 1',
reviewers: {
REVIEWER: [accounts[0]],
CC: [accounts[1]],
},
},
{
...createChange(),
_number: 2 as NumericChangeId,
subject: 'Subject 2',
},
];
let saveChangeReviewStub: sinon.SinonStub;
setup(async () => {
saveChangeReviewStub = stubRestApi('saveChangeReview').resolves(
new Response()
);
stubRestApi('getDetailedChangesWithActions').resolves([
{...changes[0], actions: {abandon: {method: HttpMethod.POST}}},
{...changes[1], status: ChangeStatus.ABANDONED},
]);
bulkActionsModel.sync(changes);
bulkActionsModel.addSelectedChangeNum(changes[0]._number);
bulkActionsModel.addSelectedChangeNum(changes[1]._number);
});
test('adds reviewers/cc only to changes that need it', async () => {
bulkActionsModel.addReviewers(
new Map([
[ReviewerState.REVIEWER, [accounts[0]]],
[ReviewerState.CC, [accounts[1]]],
])
);
// changes[0] is not updated since it already has the reviewer & CC
assert.isTrue(saveChangeReviewStub.calledOnce);
assert.sameDeepOrderedMembers(saveChangeReviewStub.firstCall.args, [
changes[1]._number,
'current',
{
reviewers: [
{reviewer: accounts[0]._account_id, state: ReviewerState.REVIEWER},
{reviewer: accounts[1]._account_id, state: ReviewerState.CC},
],
},
]);
});
});
suite('voteChanges', () => {
let detailedActionsStub: SinonStubbedMember<
RestApiService['getDetailedChangesWithActions']
>;
setup(async () => {
const c1 = {...createChange(), revisions: createRevisions(10)};
c1._number = 1 as NumericChangeId;
const c2 = {...createChange(), revisions: createRevisions(4)};
c2._number = 2 as NumericChangeId;
detailedActionsStub = stubRestApi('getDetailedChangesWithActions');
detailedActionsStub.returns(
Promise.resolve([
{...c1, actions: {abandon: {method: HttpMethod.POST}}},
{...c2, status: ChangeStatus.ABANDONED},
])
);
await bulkActionsModel.sync([c1, c2]);
bulkActionsModel.addSelectedChangeNum(c1._number);
bulkActionsModel.addSelectedChangeNum(c2._number);
});
test('vote changes', () => {
const reviewStub = stubRestApi('saveChangeReview');
const reviewInput: ReviewInput = {
labels: {
a: 1,
},
};
bulkActionsModel.voteChanges(reviewInput);
assert.equal(reviewStub.callCount, 2);
assert.deepEqual(reviewStub.firstCall.args.slice(0, 3), [
1 as NumericChangeId,
'current',
{
labels: {
a: 1,
},
},
]);
assert.deepEqual(reviewStub.secondCall.args.slice(0, 3), [
2 as NumericChangeId,
'current',
{
labels: {
a: 1,
},
},
]);
});
});
test('stale changes are removed from the model', async () => {
const c1 = createChange();
c1._number = 1 as NumericChangeId;
const c2 = createChange();
c2._number = 2 as NumericChangeId;
bulkActionsModel.sync([c1, c2]);
bulkActionsModel.addSelectedChangeNum(c1._number);
bulkActionsModel.addSelectedChangeNum(c2._number);
let selectedChangeNums = await waitUntilObserved(
bulkActionsModel!.selectedChangeNums$,
s => s.length === 2
);
let totalChangeCount = await waitUntilObserved(
bulkActionsModel.totalChangeCount$,
totalChangeCount => totalChangeCount === 2
);
assert.sameMembers(selectedChangeNums, [c1._number, c2._number]);
assert.equal(totalChangeCount, 2);
bulkActionsModel.sync([c1]);
selectedChangeNums = await waitUntilObserved(
bulkActionsModel!.selectedChangeNums$,
s => s.length === 1
);
totalChangeCount = await waitUntilObserved(
bulkActionsModel.totalChangeCount$,
totalChangeCount => totalChangeCount === 1
);
assert.sameMembers(selectedChangeNums, [c1._number]);
assert.equal(totalChangeCount, 1);
});
test('sync fetches new changes', async () => {
const c1 = createChange();
c1._number = 1 as NumericChangeId;
const c2 = createChange();
c2._number = 2 as NumericChangeId;
assert.equal(
bulkActionsModel.getState().loadingState,
LoadingState.NOT_SYNCED
);
bulkActionsModel.sync([c1, c2]);
await waitUntilObserved(
bulkActionsModel.loadingState$,
s => s === LoadingState.LOADING
);
await waitUntilObserved(
bulkActionsModel.loadingState$,
s => s === LoadingState.LOADED
);
const model = bulkActionsModel.getState();
assert.strictEqual(
model.allChanges.get(1 as NumericChangeId)?.subject,
'Subject 1'
);
assert.strictEqual(
model.allChanges.get(2 as NumericChangeId)?.subject,
'Subject 2'
);
});
test('sync retains keys from original change including reviewers', async () => {
const c1: ChangeInfo = {
...createChange(),
_number: 1 as NumericChangeId,
submit_requirements: [
{
name: 'a',
status: SubmitRequirementStatus.FORCED,
submittability_expression_result: {
expression: 'b',
},
},
],
reviewers: {
REVIEWER: [{_account_id: 1 as AccountId, display_name: 'MyName'}],
},
};
stubRestApi('getDetailedChangesWithActions').callsFake(() => {
const change: ChangeInfo = {
...createChange(),
_number: 1 as NumericChangeId,
actions: {abandon: {}},
// detailed data will be missing names
reviewers: {REVIEWER: [createAccountWithIdNameAndEmail()]},
};
assert.isNotOk(change.submit_requirements);
return Promise.resolve([change]);
});
bulkActionsModel.sync([c1]);
await waitUntilObserved(
bulkActionsModel.loadingState$,
s => s === LoadingState.LOADED
);
const changeAfterSync = bulkActionsModel
.getState()
.allChanges.get(1 as NumericChangeId);
assert.deepEqual(changeAfterSync!.submit_requirements, [
{
name: 'a',
status: SubmitRequirementStatus.FORCED,
submittability_expression_result: {
expression: 'b',
},
},
]);
assert.deepEqual(changeAfterSync!.actions, {abandon: {}});
// original reviewers are kept, which includes more details than loaded ones
assert.deepEqual(changeAfterSync!.reviewers, c1.reviewers);
});
test('sync ignores outdated fetch responses', async () => {
const c1 = createChange();
c1._number = 1 as NumericChangeId;
const c2 = createChange();
c2._number = 2 as NumericChangeId;
const responsePromise1 = mockPromise<ChangeInfo[]>();
let promise = responsePromise1;
const getChangesStub = stubRestApi(
'getDetailedChangesWithActions'
).callsFake(() => promise);
bulkActionsModel.sync([c1, c2]);
assert.strictEqual(getChangesStub.callCount, 1);
await waitUntilObserved(
bulkActionsModel.loadingState$,
s => s === LoadingState.LOADING
);
const responsePromise2 = mockPromise<ChangeInfo[]>();
promise = responsePromise2;
bulkActionsModel.sync([c1, c2]);
assert.strictEqual(getChangesStub.callCount, 2);
responsePromise2.resolve([
{...createChange(), _number: 1, subject: 'Subject 1'},
{...createChange(), _number: 2, subject: 'Subject 2'},
] as ChangeInfo[]);
await waitUntilObserved(
bulkActionsModel.loadingState$,
s => s === LoadingState.LOADED
);
const model = bulkActionsModel.getState();
assert.strictEqual(
model.allChanges.get(1 as NumericChangeId)?.subject,
'Subject 1'
);
assert.strictEqual(
model.allChanges.get(2 as NumericChangeId)?.subject,
'Subject 2'
);
// Resolve the old promise.
responsePromise1.resolve([
{...createChange(), _number: 1, subject: 'Subject 1-old'},
{...createChange(), _number: 2, subject: 'Subject 2-old'},
] as ChangeInfo[]);
await flush();
const model2 = bulkActionsModel.getState();
// No change should happen.
assert.strictEqual(
model2.allChanges.get(1 as NumericChangeId)?.subject,
'Subject 1'
);
assert.strictEqual(
model2.allChanges.get(2 as NumericChangeId)?.subject,
'Subject 2'
);
});
});