| /** |
| * @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' |
| ); |
| }); |
| }); |