| /** |
| * @license |
| * Copyright (C) 2015 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| import '../../../test/common-test-setup-karma'; |
| import './gr-reply-dialog'; |
| import { |
| mockPromise, |
| queryAll, |
| queryAndAssert, |
| stubStorage, |
| } from '../../../test/test-utils'; |
| import { |
| ChangeStatus, |
| ReviewerState, |
| SpecialFilePath, |
| } from '../../../constants/constants'; |
| import {appContext} from '../../../services/app-context'; |
| import {addListenerForTest} from '../../../test/test-utils'; |
| import {stubRestApi} from '../../../test/test-utils'; |
| import {JSON_PREFIX} from '../../shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper'; |
| import {CODE_REVIEW} from '../../../utils/label-util'; |
| import { |
| createAccountWithId, |
| createChange, |
| createCommentThread, |
| createDraft, |
| createRevision, |
| } from '../../../test/test-data-generators'; |
| import { |
| pressAndReleaseKeyOn, |
| tap, |
| } from '@polymer/iron-test-helpers/mock-interactions'; |
| import {GrReplyDialog} from './gr-reply-dialog'; |
| import { |
| AccountId, |
| AccountInfo, |
| CommitId, |
| DetailedLabelInfo, |
| GroupId, |
| GroupName, |
| NumericChangeId, |
| PatchSetNum, |
| ReviewerInput, |
| ReviewInput, |
| ReviewResult, |
| Suggestion, |
| UrlEncodedCommentId, |
| } from '../../../types/common'; |
| import {CommentThread} from '../../../utils/comment-util'; |
| import {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces'; |
| import { |
| AccountInfoInput, |
| GrAccountList, |
| } from '../../shared/gr-account-list/gr-account-list'; |
| import {GrLabelScoreRow} from '../gr-label-score-row/gr-label-score-row'; |
| import {GrLabelScores} from '../gr-label-scores/gr-label-scores'; |
| import {GrThreadList} from '../gr-thread-list/gr-thread-list'; |
| import {GrOverlay} from '../../shared/gr-overlay/gr-overlay'; |
| |
| const basicFixture = fixtureFromElement('gr-reply-dialog'); |
| |
| function cloneableResponse(status: number, text: string) { |
| return { |
| ...new Response(), |
| ok: false, |
| status, |
| text() { |
| return Promise.resolve(text); |
| }, |
| clone() { |
| return { |
| ok: false, |
| status, |
| text() { |
| return Promise.resolve(text); |
| }, |
| }; |
| }, |
| }; |
| } |
| |
| suite('gr-reply-dialog tests', () => { |
| let element: GrReplyDialog; |
| let changeNum: NumericChangeId; |
| let patchNum: PatchSetNum; |
| |
| let getDraftCommentStub: sinon.SinonStub; |
| let setDraftCommentStub: sinon.SinonStub; |
| let eraseDraftCommentStub: sinon.SinonStub; |
| |
| const emptyAccountInfoInputChanges = ([] as unknown) as PolymerDeepPropertyChange< |
| AccountInfoInput[], |
| AccountInfoInput[] |
| >; |
| |
| let lastId = 1; |
| const makeAccount = function () { |
| return {_account_id: lastId++ as AccountId}; |
| }; |
| const makeGroup = function () { |
| return {id: `${lastId++}` as GroupId}; |
| }; |
| |
| setup(() => { |
| changeNum = 42 as NumericChangeId; |
| patchNum = 1 as PatchSetNum; |
| |
| stubRestApi('getChange').returns(Promise.resolve({...createChange()})); |
| stubRestApi('getChangeSuggestedReviewers').returns(Promise.resolve([])); |
| |
| sinon.stub(appContext.flagsService, 'isEnabled').returns(true); |
| |
| element = basicFixture.instantiate(); |
| element.change = { |
| ...createChange(), |
| _number: changeNum, |
| owner: { |
| _account_id: (999 as AccountId) as AccountId, |
| display_name: 'Kermit', |
| }, |
| labels: { |
| Verified: { |
| values: { |
| '-1': 'Fails', |
| ' 0': 'No score', |
| '+1': 'Verified', |
| }, |
| default_value: 0, |
| }, |
| 'Code-Review': { |
| values: { |
| '-2': 'Do not submit', |
| '-1': "I would prefer that you didn't submit this", |
| ' 0': 'No score', |
| '+1': 'Looks good to me, but someone else must approve', |
| '+2': 'Looks good to me, approved', |
| }, |
| default_value: 0, |
| }, |
| }, |
| }; |
| element.patchNum = patchNum; |
| element.permittedLabels = { |
| 'Code-Review': ['-1', ' 0', '+1'], |
| Verified: ['-1', ' 0', '+1'], |
| }; |
| |
| getDraftCommentStub = stubStorage('getDraftComment'); |
| setDraftCommentStub = stubStorage('setDraftComment'); |
| eraseDraftCommentStub = stubStorage('eraseDraftComment'); |
| |
| // sinon.stub(patchSetUtilMockProxy, 'fetchChangeUpdates') |
| // .returns(Promise.resolve({isLatest: true})); |
| |
| // Allow the elements created by dom-repeat to be stamped. |
| flush(); |
| }); |
| |
| function stubSaveReview( |
| jsonResponseProducer: (input: ReviewInput) => ReviewResult | void |
| ) { |
| return sinon.stub(element, '_saveReview').callsFake( |
| review => |
| new Promise((resolve, reject) => { |
| try { |
| const result = jsonResponseProducer(review) || {}; |
| const resultStr = JSON_PREFIX + JSON.stringify(result); |
| resolve({ |
| ...new Response(), |
| ok: true, |
| text() { |
| return Promise.resolve(resultStr); |
| }, |
| }); |
| } catch (err) { |
| reject(err); |
| } |
| }) |
| ); |
| } |
| |
| function interceptSaveReview() { |
| let resolver: (review: ReviewInput) => void; |
| const promise = new Promise(resolve => { |
| resolver = resolve; |
| }); |
| stubSaveReview((review: ReviewInput) => { |
| resolver(review); |
| }); |
| return promise; |
| } |
| |
| test('default to publishing draft comments with reply', async () => { |
| // Async tick is needed because iron-selector content is distributed and |
| // distributed content requires an observer to be set up. |
| await flush(); |
| element.draft = 'I wholeheartedly disapprove'; |
| const saveReviewPromise = interceptSaveReview(); |
| |
| // This is needed on non-Blink engines most likely due to the ways in |
| // which the dom-repeat elements are stamped. |
| await flush(); |
| tap(queryAndAssert(element, '.send')); |
| |
| const review = await saveReviewPromise; |
| assert.deepEqual(review, { |
| drafts: 'PUBLISH_ALL_REVISIONS', |
| labels: { |
| 'Code-Review': 0, |
| Verified: 0, |
| }, |
| comments: { |
| [SpecialFilePath.PATCHSET_LEVEL_COMMENTS]: [ |
| { |
| message: 'I wholeheartedly disapprove', |
| unresolved: false, |
| }, |
| ], |
| }, |
| reviewers: [], |
| add_to_attention_set: [], |
| remove_from_attention_set: [], |
| ignore_automatic_attention_set_rules: true, |
| }); |
| assert.isFalse( |
| (queryAndAssert(element, '#commentList') as GrThreadList).hidden |
| ); |
| }); |
| |
| test('modified attention set', done => { |
| element._newAttentionSet = new Set([314 as AccountId]); |
| const buttonEl = queryAndAssert(element, '.edit-attention-button'); |
| tap(buttonEl); |
| flush(); |
| |
| stubSaveReview((review: ReviewInput) => { |
| assert.isTrue(review?.ignore_automatic_attention_set_rules); |
| assert.deepEqual(review?.add_to_attention_set, [ |
| { |
| user: 314 as AccountId, |
| reason: 'Anonymous replied on the change', |
| }, |
| ]); |
| assert.deepEqual(review?.remove_from_attention_set, []); |
| done(); |
| }); |
| tap(queryAndAssert(element, '.send')); |
| }); |
| |
| function checkComputeAttention( |
| status: ChangeStatus, |
| userId?: AccountId, |
| reviewerIds?: AccountId[], |
| ownerId?: AccountId, |
| attSetIds?: AccountId[], |
| replyToIds?: AccountId[], |
| expectedIds?: AccountId[], |
| uploaderId?: AccountId, |
| hasDraft = true, |
| includeComments = true |
| ) { |
| const user = {_account_id: userId}; |
| const reviewers = { |
| base: reviewerIds?.map(id => { |
| return {_account_id: id}; |
| }), |
| } as PolymerDeepPropertyChange<AccountInfoInput[], AccountInfoInput[]>; |
| let draftThreads: CommentThread[] = []; |
| if (hasDraft) { |
| draftThreads = [ |
| { |
| ...createCommentThread([ |
| { |
| ...createDraft(), |
| __draft: true, |
| unresolved: true, |
| }, |
| ]), |
| }, |
| ]; |
| } |
| replyToIds?.forEach(id => |
| draftThreads[0].comments.push({ |
| author: {_account_id: id}, |
| }) |
| ); |
| const change = { |
| ...createChange(), |
| owner: {_account_id: ownerId}, |
| status, |
| }; |
| attSetIds?.forEach(id => { |
| if (!change.attention_set) change.attention_set = {}; |
| change.attention_set[id.toString()] = { |
| account: createAccountWithId(id), |
| }; |
| }); |
| if (uploaderId) { |
| change.current_revision = 'b' as CommitId; |
| change.revisions = { |
| a: createRevision(1), |
| b: {...createRevision(2), uploader: {_account_id: uploaderId}}, |
| }; |
| } |
| element.change = change; |
| element._reviewers = reviewers.base!; |
| |
| flush(); |
| const hasDrafts = draftThreads.length > 0; |
| element._computeNewAttention( |
| user, |
| reviewers!, |
| emptyAccountInfoInputChanges, |
| change, |
| draftThreads, |
| includeComments, |
| undefined, |
| hasDrafts |
| ); |
| assert.sameMembers([...element._newAttentionSet], expectedIds!); |
| } |
| |
| test('computeNewAttention NEW', () => { |
| checkComputeAttention( |
| ChangeStatus.NEW, |
| 1 as AccountId, |
| [], |
| 999 as AccountId, |
| [], |
| [], |
| [999 as AccountId] |
| ); |
| checkComputeAttention( |
| ChangeStatus.NEW, |
| 1 as AccountId, |
| [], |
| 999 as AccountId, |
| [1 as AccountId], |
| [], |
| [999 as AccountId] |
| ); |
| checkComputeAttention( |
| ChangeStatus.NEW, |
| 1 as AccountId, |
| [22 as AccountId], |
| 999 as AccountId, |
| [], |
| [], |
| [999 as AccountId] |
| ); |
| checkComputeAttention( |
| ChangeStatus.NEW, |
| 1 as AccountId, |
| [22 as AccountId], |
| 999 as AccountId, |
| [22 as AccountId], |
| [], |
| [22 as AccountId, 999 as AccountId] |
| ); |
| checkComputeAttention( |
| ChangeStatus.NEW, |
| 1 as AccountId, |
| [22 as AccountId], |
| 999 as AccountId, |
| [], |
| [22 as AccountId], |
| [22 as AccountId, 999 as AccountId] |
| ); |
| checkComputeAttention( |
| ChangeStatus.NEW, |
| 1 as AccountId, |
| [22 as AccountId, 33 as AccountId], |
| 999 as AccountId, |
| [33 as AccountId], |
| [22 as AccountId], |
| [22 as AccountId, 33 as AccountId, 999 as AccountId] |
| ); |
| // If the owner replies, then do not add them. |
| checkComputeAttention( |
| ChangeStatus.NEW, |
| 1 as AccountId, |
| [], |
| 1 as AccountId, |
| [], |
| [], |
| [] |
| ); |
| checkComputeAttention( |
| ChangeStatus.NEW, |
| 1 as AccountId, |
| [], |
| 1 as AccountId, |
| [1 as AccountId], |
| [], |
| [] |
| ); |
| checkComputeAttention( |
| ChangeStatus.NEW, |
| 1 as AccountId, |
| [22 as AccountId], |
| 1 as AccountId, |
| [], |
| [], |
| [] |
| ); |
| |
| checkComputeAttention( |
| ChangeStatus.NEW, |
| 1 as AccountId, |
| [22 as AccountId], |
| 1 as AccountId, |
| [], |
| [22 as AccountId], |
| [22 as AccountId] |
| ); |
| checkComputeAttention( |
| ChangeStatus.NEW, |
| 1 as AccountId, |
| [22 as AccountId, 33 as AccountId], |
| 1 as AccountId, |
| [33 as AccountId], |
| [22 as AccountId], |
| [22 as AccountId, 33 as AccountId] |
| ); |
| checkComputeAttention( |
| ChangeStatus.NEW, |
| 1 as AccountId, |
| [22 as AccountId, 33 as AccountId], |
| 1 as AccountId, |
| [], |
| [22 as AccountId], |
| [22 as AccountId] |
| ); |
| checkComputeAttention( |
| ChangeStatus.NEW, |
| 1 as AccountId, |
| [22 as AccountId, 33 as AccountId], |
| 1 as AccountId, |
| [], |
| [22 as AccountId, 33 as AccountId], |
| [22 as AccountId, 33 as AccountId] |
| ); |
| checkComputeAttention( |
| ChangeStatus.NEW, |
| 1 as AccountId, |
| [22 as AccountId, 33 as AccountId], |
| 1 as AccountId, |
| [22 as AccountId, 33 as AccountId], |
| [], |
| [22 as AccountId, 33 as AccountId] |
| ); |
| // with uploader |
| checkComputeAttention( |
| ChangeStatus.NEW, |
| 1 as AccountId, |
| [], |
| 1 as AccountId, |
| [], |
| [2 as AccountId], |
| [2 as AccountId], |
| 2 as AccountId |
| ); |
| checkComputeAttention( |
| ChangeStatus.NEW, |
| 1 as AccountId, |
| [], |
| 1 as AccountId, |
| [2 as AccountId], |
| [], |
| [2 as AccountId], |
| 2 as AccountId |
| ); |
| checkComputeAttention( |
| ChangeStatus.NEW, |
| 1 as AccountId, |
| [], |
| 3 as AccountId, |
| [], |
| [], |
| [2 as AccountId, 3 as AccountId], |
| 2 as AccountId |
| ); |
| }); |
| |
| test('computeNewAttention MERGED', () => { |
| checkComputeAttention( |
| ChangeStatus.MERGED, |
| undefined, |
| [], |
| 999 as AccountId, |
| [], |
| [], |
| [], |
| undefined, |
| false |
| ); |
| checkComputeAttention( |
| ChangeStatus.MERGED, |
| 1 as AccountId, |
| [], |
| 999 as AccountId, |
| [], |
| [], |
| [], |
| undefined, |
| false |
| ); |
| checkComputeAttention( |
| ChangeStatus.MERGED, |
| 1 as AccountId, |
| [], |
| 999 as AccountId, |
| [], |
| [], |
| [999 as AccountId], |
| undefined, |
| true |
| ); |
| checkComputeAttention( |
| ChangeStatus.MERGED, |
| 1 as AccountId, |
| [], |
| 999 as AccountId, |
| [], |
| [], |
| [], |
| undefined, |
| true, |
| false |
| ); |
| checkComputeAttention( |
| ChangeStatus.MERGED, |
| 1 as AccountId, |
| [], |
| 999 as AccountId, |
| [1 as AccountId], |
| [], |
| [], |
| undefined, |
| false |
| ); |
| checkComputeAttention( |
| ChangeStatus.MERGED, |
| 1 as AccountId, |
| [22 as AccountId], |
| 999 as AccountId, |
| [], |
| [], |
| [], |
| undefined, |
| false |
| ); |
| checkComputeAttention( |
| ChangeStatus.MERGED, |
| 1 as AccountId, |
| [22 as AccountId], |
| 999 as AccountId, |
| [22 as AccountId], |
| [], |
| [22 as AccountId], |
| undefined, |
| false |
| ); |
| checkComputeAttention( |
| ChangeStatus.MERGED, |
| 1 as AccountId, |
| [22 as AccountId], |
| 999 as AccountId, |
| [], |
| [22 as AccountId], |
| [] |
| ); |
| checkComputeAttention( |
| ChangeStatus.MERGED, |
| 1 as AccountId, |
| [22 as AccountId, 33 as AccountId], |
| 999 as AccountId, |
| [33 as AccountId], |
| [22 as AccountId], |
| [33 as AccountId] |
| ); |
| checkComputeAttention( |
| ChangeStatus.MERGED, |
| 1 as AccountId, |
| [], |
| 1 as AccountId, |
| [], |
| [], |
| [] |
| ); |
| checkComputeAttention( |
| ChangeStatus.MERGED, |
| 1 as AccountId, |
| [], |
| 1 as AccountId, |
| [], |
| [], |
| [], |
| undefined, |
| true |
| ); |
| checkComputeAttention( |
| ChangeStatus.MERGED, |
| 1 as AccountId, |
| [], |
| 1 as AccountId, |
| [1 as AccountId], |
| [], |
| [] |
| ); |
| checkComputeAttention( |
| ChangeStatus.MERGED, |
| 1 as AccountId, |
| [], |
| 1 as AccountId, |
| [1 as AccountId], |
| [], |
| [], |
| undefined, |
| true |
| ); |
| checkComputeAttention( |
| ChangeStatus.MERGED, |
| 1 as AccountId, |
| [22 as AccountId], |
| 1 as AccountId, |
| [], |
| [], |
| [] |
| ); |
| checkComputeAttention( |
| ChangeStatus.MERGED, |
| 1 as AccountId, |
| [22 as AccountId], |
| 1 as AccountId, |
| [], |
| [22 as AccountId], |
| [] |
| ); |
| checkComputeAttention( |
| ChangeStatus.MERGED, |
| 1 as AccountId, |
| [22 as AccountId, 33 as AccountId], |
| 1 as AccountId, |
| [33 as AccountId], |
| [22 as AccountId], |
| [33 as AccountId] |
| ); |
| checkComputeAttention( |
| ChangeStatus.MERGED, |
| 1 as AccountId, |
| [22 as AccountId, 33 as AccountId], |
| 1 as AccountId, |
| [], |
| [22 as AccountId], |
| [] |
| ); |
| checkComputeAttention( |
| ChangeStatus.MERGED, |
| 1 as AccountId, |
| [22 as AccountId, 33 as AccountId], |
| 1 as AccountId, |
| [], |
| [22 as AccountId, 33 as AccountId], |
| [] |
| ); |
| checkComputeAttention( |
| ChangeStatus.MERGED, |
| 1 as AccountId, |
| [22 as AccountId, 33 as AccountId], |
| 1 as AccountId, |
| [22 as AccountId, 33 as AccountId], |
| [], |
| [22 as AccountId, 33 as AccountId] |
| ); |
| }); |
| |
| test('computeNewAttention when adding reviewers', () => { |
| const user = {_account_id: 1 as AccountId}; |
| const reviewers = { |
| base: [ |
| {_account_id: 1 as AccountId, _pendingAdd: true}, |
| {_account_id: 2 as AccountId, _pendingAdd: true}, |
| ], |
| } as PolymerDeepPropertyChange<AccountInfoInput[], AccountInfoInput[]>; |
| const change = { |
| ...createChange(), |
| owner: {_account_id: 5 as AccountId}, |
| status: ChangeStatus.NEW, |
| attention_set: {}, |
| }; |
| element.change = change; |
| element._reviewers = reviewers.base; |
| flush(); |
| |
| element._computeNewAttention( |
| user, |
| reviewers, |
| emptyAccountInfoInputChanges, |
| change, |
| [], |
| true |
| ); |
| assert.sameMembers([...element._newAttentionSet], [1, 2]); |
| |
| // If the user votes on the change, then they should not be added to the |
| // attention set, even if they have just added themselves as reviewer. |
| // But voting should also add the owner (5). |
| const labelsChanged = true; |
| element._computeNewAttention( |
| user, |
| reviewers, |
| emptyAccountInfoInputChanges, |
| change, |
| [], |
| true, |
| labelsChanged |
| ); |
| assert.sameMembers([...element._newAttentionSet], [2, 5]); |
| }); |
| |
| test('computeNewAttention when sending wip change for review', () => { |
| const reviewers = { |
| base: [{...createAccountWithId(2)}, {...createAccountWithId(3)}], |
| } as PolymerDeepPropertyChange<AccountInfoInput[], AccountInfoInput[]>; |
| const change = { |
| ...createChange(), |
| owner: {_account_id: 1 as AccountId}, |
| status: ChangeStatus.NEW, |
| attention_set: {}, |
| }; |
| element.change = change; |
| element._reviewers = reviewers.base; |
| flush(); |
| |
| // For an active change there is no reason to add anyone to the set. |
| let user = {_account_id: 1 as AccountId}; |
| element._computeNewAttention( |
| user, |
| reviewers, |
| emptyAccountInfoInputChanges, |
| change, |
| [], |
| false |
| ); |
| assert.sameMembers([...element._newAttentionSet], []); |
| |
| // If the change is "work in progress" and the owner sends a reply, then |
| // add all reviewers. |
| element.canBeStarted = true; |
| flush(); |
| user = {_account_id: 1 as AccountId}; |
| element._computeNewAttention( |
| user, |
| reviewers, |
| emptyAccountInfoInputChanges, |
| change, |
| [], |
| false |
| ); |
| assert.sameMembers([...element._newAttentionSet], [2, 3]); |
| |
| // ... but not when someone else replies. |
| user = {_account_id: 4 as AccountId}; |
| element._computeNewAttention( |
| user, |
| reviewers, |
| emptyAccountInfoInputChanges, |
| change, |
| [], |
| false |
| ); |
| assert.sameMembers([...element._newAttentionSet], []); |
| }); |
| |
| test('computeNewAttentionAccounts', () => { |
| element._reviewers = [ |
| {_account_id: 123 as AccountId, display_name: 'Ernie'}, |
| {_account_id: 321 as AccountId, display_name: 'Bert'}, |
| ]; |
| element._ccs = [{_account_id: 7 as AccountId, display_name: 'Elmo'}]; |
| const compute = (currentAtt: AccountId[], newAtt: AccountId[]) => |
| element |
| ._computeNewAttentionAccounts( |
| undefined, |
| new Set(currentAtt), |
| new Set(newAtt) |
| ) |
| .map(a => a!._account_id); |
| |
| assert.sameMembers(compute([], []), []); |
| assert.sameMembers(compute([], [999 as AccountId]), [999 as AccountId]); |
| assert.sameMembers(compute([999 as AccountId], []), []); |
| assert.sameMembers(compute([999 as AccountId], [999 as AccountId]), []); |
| assert.sameMembers( |
| compute([123 as AccountId, 321 as AccountId], [999 as AccountId]), |
| [999 as AccountId] |
| ); |
| assert.sameMembers( |
| compute( |
| [999 as AccountId], |
| [7 as AccountId, 123 as AccountId, 999 as AccountId] |
| ), |
| [7 as AccountId, 123 as AccountId] |
| ); |
| }); |
| |
| test('_computeCommentAccounts', () => { |
| element.change = { |
| ...createChange(), |
| labels: { |
| 'Code-Review': { |
| all: [ |
| {_account_id: 1 as AccountId, value: 0}, |
| {_account_id: 2 as AccountId, value: 1}, |
| {_account_id: 3 as AccountId, value: 2}, |
| ], |
| values: { |
| '-2': 'Do not submit', |
| '-1': 'I would prefer that you didnt submit this', |
| ' 0': 'No score', |
| '+1': 'Looks good to me, but someone else must approve', |
| '+2': 'Looks good to me, approved', |
| }, |
| }, |
| }, |
| }; |
| const threads = [ |
| { |
| ...createCommentThread([ |
| { |
| id: '1' as UrlEncodedCommentId, |
| author: {_account_id: 1 as AccountId}, |
| unresolved: false, |
| }, |
| { |
| id: '2' as UrlEncodedCommentId, |
| in_reply_to: '1' as UrlEncodedCommentId, |
| author: {_account_id: 2 as AccountId}, |
| unresolved: true, |
| }, |
| ]), |
| }, |
| { |
| ...createCommentThread([ |
| { |
| id: '3' as UrlEncodedCommentId, |
| author: {_account_id: 3 as AccountId}, |
| unresolved: false, |
| }, |
| { |
| id: '4' as UrlEncodedCommentId, |
| in_reply_to: '3' as UrlEncodedCommentId, |
| author: {_account_id: 4 as AccountId}, |
| unresolved: false, |
| }, |
| ]), |
| }, |
| ]; |
| const actualAccounts = [...element._computeCommentAccounts(threads)]; |
| // Account 3 is not included, because the comment is resolved *and* they |
| // have given the highest possible vote on the Code-Review label. |
| assert.sameMembers(actualAccounts, [1, 2, 4]); |
| }); |
| |
| test('toggle resolved checkbox', async () => { |
| const checkboxEl = queryAndAssert( |
| element, |
| '#resolvedPatchsetLevelCommentCheckbox' |
| ); |
| tap(checkboxEl); |
| |
| // Async tick is needed because iron-selector content is distributed and |
| // distributed content requires an observer to be set up. |
| await flush(); |
| element.draft = 'I wholeheartedly disapprove'; |
| const saveReviewPromise = interceptSaveReview(); |
| |
| // This is needed on non-Blink engines most likely due to the ways in |
| // which the dom-repeat elements are stamped. |
| await flush(); |
| tap(queryAndAssert(element, '.send')); |
| |
| const review = await saveReviewPromise; |
| assert.deepEqual(review, { |
| drafts: 'PUBLISH_ALL_REVISIONS', |
| labels: { |
| 'Code-Review': 0, |
| Verified: 0, |
| }, |
| comments: { |
| [SpecialFilePath.PATCHSET_LEVEL_COMMENTS]: [ |
| { |
| message: 'I wholeheartedly disapprove', |
| unresolved: true, |
| }, |
| ], |
| }, |
| reviewers: [], |
| add_to_attention_set: [], |
| remove_from_attention_set: [], |
| ignore_automatic_attention_set_rules: true, |
| }); |
| }); |
| |
| test('label picker', async () => { |
| element.draft = 'I wholeheartedly disapprove'; |
| const saveReviewPromise = interceptSaveReview(); |
| |
| sinon.stub(element.getLabelScores(), 'getLabelValues').callsFake(() => { |
| return { |
| 'Code-Review': -1, |
| Verified: -1, |
| }; |
| }); |
| |
| // This is needed on non-Blink engines most likely due to the ways in |
| // which the dom-repeat elements are stamped. |
| await flush(); |
| tap(queryAndAssert(element, '.send')); |
| assert.isTrue(element.disabled); |
| |
| const review = await saveReviewPromise; |
| await flush(); |
| assert.isFalse( |
| element.disabled, |
| 'Element should be enabled when done sending reply.' |
| ); |
| assert.equal(element.draft.length, 0); |
| assert.deepEqual(review, { |
| drafts: 'PUBLISH_ALL_REVISIONS', |
| labels: { |
| 'Code-Review': -1, |
| Verified: -1, |
| }, |
| comments: { |
| [SpecialFilePath.PATCHSET_LEVEL_COMMENTS]: [ |
| { |
| message: 'I wholeheartedly disapprove', |
| unresolved: false, |
| }, |
| ], |
| }, |
| reviewers: [], |
| add_to_attention_set: [], |
| remove_from_attention_set: [], |
| ignore_automatic_attention_set_rules: true, |
| }); |
| }); |
| |
| test('keep draft comments with reply', async () => { |
| tap(queryAndAssert(element, '#includeComments')); |
| assert.equal(element._includeComments, false); |
| |
| // Async tick is needed because iron-selector content is distributed and |
| // distributed content requires an observer to be set up. |
| await flush(); |
| element.draft = 'I wholeheartedly disapprove'; |
| const saveReviewPromise = interceptSaveReview(); |
| |
| // This is needed on non-Blink engines most likely due to the ways in |
| // which the dom-repeat elements are stamped. |
| await flush(); |
| tap(queryAndAssert(element, '.send')); |
| |
| const review = await saveReviewPromise; |
| await flush(); |
| assert.deepEqual(review, { |
| drafts: 'KEEP', |
| labels: { |
| 'Code-Review': 0, |
| Verified: 0, |
| }, |
| comments: { |
| [SpecialFilePath.PATCHSET_LEVEL_COMMENTS]: [ |
| { |
| message: 'I wholeheartedly disapprove', |
| unresolved: false, |
| }, |
| ], |
| }, |
| reviewers: [], |
| add_to_attention_set: [], |
| remove_from_attention_set: [], |
| ignore_automatic_attention_set_rules: true, |
| }); |
| }); |
| |
| test('getlabelValue returns value', done => { |
| flush(() => { |
| const el = queryAndAssert( |
| queryAndAssert(element, 'gr-label-scores'), |
| 'gr-label-score-row[name="Verified"]' |
| ) as GrLabelScoreRow; |
| el.setSelectedValue('-1'); |
| assert.equal('-1', element.getLabelValue('Verified')); |
| done(); |
| }); |
| }); |
| |
| test('getlabelValue when no score is selected', done => { |
| flush(() => { |
| const el = queryAndAssert( |
| queryAndAssert(element, 'gr-label-scores'), |
| 'gr-label-score-row[name="Code-Review"]' |
| ) as GrLabelScoreRow; |
| el.setSelectedValue('-1'); |
| assert.strictEqual(element.getLabelValue('Verified'), ' 0'); |
| done(); |
| }); |
| }); |
| |
| test('setlabelValue', done => { |
| element._account = {_account_id: 1 as AccountId}; |
| flush(() => { |
| const label = 'Verified'; |
| const value = '+1'; |
| element.setLabelValue(label, value); |
| |
| const labels = (queryAndAssert( |
| element, |
| '#labelScores' |
| ) as GrLabelScores).getLabelValues(); |
| assert.deepEqual(labels, { |
| 'Code-Review': 0, |
| Verified: 1, |
| }); |
| done(); |
| }); |
| }); |
| |
| function getActiveElement() { |
| return document.activeElement; |
| } |
| |
| function isVisible(el: Element) { |
| assert.ok(el); |
| return getComputedStyle(el).getPropertyValue('display') !== 'none'; |
| } |
| |
| function overlayObserver(mode: string) { |
| return new Promise(resolve => { |
| function listener() { |
| element.removeEventListener('iron-overlay-' + mode, listener); |
| resolve(mode); |
| } |
| element.addEventListener('iron-overlay-' + mode, listener); |
| }); |
| } |
| |
| function isFocusInsideElement(element: Element) { |
| // In Polymer 2 focused element either <paper-input> or nested |
| // native input <input> element depending on the current focus |
| // in browser window. |
| // For example, the focus is changed if the developer console |
| // get a focus. |
| let activeElement = getActiveElement(); |
| while (activeElement) { |
| if (activeElement === element) { |
| return true; |
| } |
| if (activeElement.parentElement) { |
| activeElement = activeElement.parentElement; |
| } else { |
| activeElement = (activeElement.getRootNode() as ShadowRoot).host; |
| } |
| } |
| return false; |
| } |
| |
| async function testConfirmationDialog(cc?: boolean) { |
| const yesButton = queryAndAssert( |
| element, |
| '.reviewerConfirmationButtons gr-button:first-child' |
| ); |
| const noButton = queryAndAssert( |
| element, |
| '.reviewerConfirmationButtons gr-button:last-child' |
| ); |
| |
| element._ccPendingConfirmation = null; |
| element._reviewerPendingConfirmation = null; |
| flush(); |
| assert.isFalse( |
| isVisible(queryAndAssert(element, 'reviewerConfirmationOverlay')) |
| ); |
| |
| // Cause the confirmation dialog to display. |
| let observer = overlayObserver('opened'); |
| const group = { |
| id: 'id' as GroupId, |
| name: 'name' as GroupName, |
| }; |
| if (cc) { |
| element._ccPendingConfirmation = { |
| group, |
| confirm: false, |
| }; |
| } else { |
| element._reviewerPendingConfirmation = { |
| group, |
| confirm: false, |
| }; |
| } |
| flush(); |
| |
| if (cc) { |
| assert.deepEqual( |
| element._ccPendingConfirmation, |
| element._pendingConfirmationDetails |
| ); |
| } else { |
| assert.deepEqual( |
| element._reviewerPendingConfirmation, |
| element._pendingConfirmationDetails |
| ); |
| } |
| |
| await observer; |
| assert.isTrue( |
| isVisible(queryAndAssert(element, 'reviewerConfirmationOverlay')) |
| ); |
| observer = overlayObserver('closed'); |
| const expected = 'Group name has 10 members'; |
| assert.notEqual( |
| (queryAndAssert( |
| element, |
| 'reviewerConfirmationOverlay' |
| ) as GrOverlay).innerText.indexOf(expected), |
| -1 |
| ); |
| tap(noButton); // close the overlay |
| |
| await observer; |
| assert.isFalse( |
| isVisible(queryAndAssert(element, 'reviewerConfirmationOverlay')) |
| ); |
| |
| // We should be focused on account entry input. |
| assert.isTrue( |
| isFocusInsideElement( |
| (queryAndAssert(element, '#reviewers') as GrAccountList).$.entry.$.input |
| .$.input |
| ) |
| ); |
| |
| // No reviewer/CC should have been added. |
| assert.equal( |
| (queryAndAssert(element, '#ccs') as GrAccountList).additions().length, |
| 0 |
| ); |
| assert.equal( |
| (queryAndAssert(element, '#reviewers') as GrAccountList).additions() |
| .length, |
| 0 |
| ); |
| |
| // Reopen confirmation dialog. |
| observer = overlayObserver('opened'); |
| if (cc) { |
| element._ccPendingConfirmation = { |
| group, |
| confirm: false, |
| }; |
| } else { |
| element._reviewerPendingConfirmation = { |
| group, |
| confirm: false, |
| }; |
| } |
| |
| await observer; |
| assert.isTrue( |
| isVisible(queryAndAssert(element, 'reviewerConfirmationOverlay')) |
| ); |
| observer = overlayObserver('closed'); |
| tap(yesButton); // Confirm the group. |
| |
| await observer; |
| assert.isFalse( |
| isVisible(queryAndAssert(element, 'reviewerConfirmationOverlay')) |
| ); |
| const additions = cc |
| ? (queryAndAssert(element, '#ccs') as GrAccountList).additions() |
| : (queryAndAssert(element, '#reviewers') as GrAccountList).additions(); |
| assert.deepEqual(additions, [ |
| { |
| group: { |
| id: 'id' as GroupId, |
| name: 'name' as GroupName, |
| confirmed: true, |
| _group: true, |
| _pendingAdd: true, |
| }, |
| }, |
| ]); |
| |
| // We should be focused on account entry input. |
| if (cc) { |
| assert.isTrue( |
| isFocusInsideElement( |
| (queryAndAssert(element, '#ccs') as GrAccountList).$.entry.$.input.$ |
| .input |
| ) |
| ); |
| } else { |
| assert.isTrue( |
| isFocusInsideElement( |
| (queryAndAssert(element, '#reviewers') as GrAccountList).$.entry.$ |
| .input.$.input |
| ) |
| ); |
| } |
| } |
| |
| test('cc confirmation', async () => { |
| testConfirmationDialog(true); |
| }); |
| |
| test('reviewer confirmation', async () => { |
| testConfirmationDialog(false); |
| }); |
| |
| test('_getStorageLocation', () => { |
| const actual = element._getStorageLocation(); |
| assert.equal(actual.changeNum, changeNum); |
| assert.equal(actual.patchNum, '@change'); |
| assert.equal(actual.path, '@change'); |
| }); |
| |
| test('_reviewersMutated when account-text-change is fired from ccs', () => { |
| flush(); |
| assert.isFalse(element._reviewersMutated); |
| assert.isTrue( |
| (queryAndAssert(element, '#ccs') as GrAccountList).allowAnyInput |
| ); |
| assert.isFalse( |
| (queryAndAssert(element, '#reviewers') as GrAccountList).allowAnyInput |
| ); |
| queryAndAssert(element, '#ccs').dispatchEvent( |
| new CustomEvent('account-text-changed', {bubbles: true, composed: true}) |
| ); |
| assert.isTrue(element._reviewersMutated); |
| }); |
| |
| test('gets draft from storage on open', () => { |
| const storedDraft = 'hello world'; |
| getDraftCommentStub.returns({message: storedDraft}); |
| element.open(); |
| assert.isTrue(getDraftCommentStub.called); |
| assert.equal(element.draft, storedDraft); |
| }); |
| |
| test('gets draft from storage even when text is already present', () => { |
| const storedDraft = 'hello world'; |
| getDraftCommentStub.returns({message: storedDraft}); |
| element.draft = 'foo bar'; |
| element.open(); |
| assert.isTrue(getDraftCommentStub.called); |
| assert.equal(element.draft, storedDraft); |
| }); |
| |
| test('blank if no stored draft', () => { |
| getDraftCommentStub.returns(null); |
| element.draft = 'foo bar'; |
| element.open(); |
| assert.isTrue(getDraftCommentStub.called); |
| assert.equal(element.draft, ''); |
| }); |
| |
| test('does not check stored draft when quote is present', () => { |
| const storedDraft = 'hello world'; |
| const quote = '> foo bar'; |
| getDraftCommentStub.returns({message: storedDraft}); |
| element.quote = quote; |
| element.open(); |
| assert.isFalse(getDraftCommentStub.called); |
| assert.equal(element.draft, quote); |
| assert.isNotOk(element.quote); |
| }); |
| |
| test('updates stored draft on edits', async () => { |
| const clock = sinon.useFakeTimers(); |
| |
| const firstEdit = 'hello'; |
| const location = element._getStorageLocation(); |
| |
| element.draft = firstEdit; |
| clock.tick(1000); |
| await flush(); |
| |
| assert.isTrue(setDraftCommentStub.calledWith(location, firstEdit)); |
| |
| element.draft = ''; |
| clock.tick(1000); |
| await flush(); |
| |
| assert.isTrue(eraseDraftCommentStub.calledWith(location)); |
| }); |
| |
| test('400 converts to human-readable server-error', done => { |
| stubRestApi('saveChangeReview').callsFake( |
| (_changeNum, _patchNum, _review, errFn) => { |
| errFn!( |
| cloneableResponse( |
| 400, |
| '....{"reviewers":{"id1":{"error":"human readable"}}}' |
| ) as Response |
| ); |
| return Promise.resolve(new Response()); |
| } |
| ); |
| |
| const listener = (event: Event) => { |
| if (event.target !== document) return; |
| (event as CustomEvent).detail.response.text().then((body: string) => { |
| if (body === 'human readable') { |
| done(); |
| } |
| }); |
| }; |
| addListenerForTest(document, 'server-error', listener); |
| |
| flush(() => { |
| element.send(false, false); |
| }); |
| }); |
| |
| test('non-json 400 is treated as a normal server-error', done => { |
| stubRestApi('saveChangeReview').callsFake( |
| (_changeNum, _patchNum, _review, errFn) => { |
| errFn!(cloneableResponse(400, 'Comment validation error!') as Response); |
| return Promise.resolve(new Response()); |
| } |
| ); |
| |
| const listener = (event: Event) => { |
| if (event.target !== document) return; |
| (event as CustomEvent).detail.response.text().then((body: string) => { |
| if (body === 'Comment validation error!') { |
| done(); |
| } |
| }); |
| }; |
| addListenerForTest(document, 'server-error', listener); |
| |
| // Async tick is needed because iron-selector content is distributed and |
| // distributed content requires an observer to be set up. |
| flush(() => { |
| element.send(false, false); |
| }); |
| }); |
| |
| test('filterReviewerSuggestion', () => { |
| const owner = makeAccount(); |
| const reviewer1 = makeAccount(); |
| const reviewer2 = makeGroup(); |
| const cc1 = makeAccount(); |
| const cc2 = makeGroup(); |
| let filter = element._filterReviewerSuggestionGenerator(false); |
| |
| element._owner = owner; |
| element._reviewers = [reviewer1, reviewer2]; |
| element._ccs = [cc1, cc2]; |
| |
| assert.isTrue(filter({account: makeAccount()} as Suggestion)); |
| assert.isTrue(filter({group: makeGroup()} as Suggestion)); |
| |
| // Owner should be excluded. |
| assert.isFalse(filter({account: owner} as Suggestion)); |
| |
| // Existing and pending reviewers should be excluded when isCC = false. |
| assert.isFalse(filter({account: reviewer1} as Suggestion)); |
| assert.isFalse(filter({group: reviewer2} as Suggestion)); |
| |
| filter = element._filterReviewerSuggestionGenerator(true); |
| |
| // Existing and pending CCs should be excluded when isCC = true;. |
| assert.isFalse(filter({account: cc1} as Suggestion)); |
| assert.isFalse(filter({group: cc2} as Suggestion)); |
| }); |
| |
| test('_focusOn', async () => { |
| const chooseFocusTargetSpy = sinon.spy(element, '_chooseFocusTarget'); |
| element._focusOn(); |
| await flush(); |
| assert.equal(chooseFocusTargetSpy.callCount, 1); |
| assert.equal(element?.shadowRoot?.activeElement?.tagName, 'GR-TEXTAREA'); |
| assert.equal(element?.shadowRoot?.activeElement?.id, 'textarea'); |
| |
| element._focusOn(element.FocusTarget.ANY); |
| await flush(); |
| assert.equal(chooseFocusTargetSpy.callCount, 2); |
| assert.equal(element?.shadowRoot?.activeElement?.tagName, 'GR-TEXTAREA'); |
| assert.equal(element?.shadowRoot?.activeElement?.id, 'textarea'); |
| |
| element._focusOn(element.FocusTarget.BODY); |
| await flush(); |
| assert.equal(chooseFocusTargetSpy.callCount, 2); |
| assert.equal(element?.shadowRoot?.activeElement?.tagName, 'GR-TEXTAREA'); |
| assert.equal(element?.shadowRoot?.activeElement?.id, 'textarea'); |
| |
| element._focusOn(element.FocusTarget.REVIEWERS); |
| await flush(); |
| assert.equal(chooseFocusTargetSpy.callCount, 2); |
| assert.equal( |
| element?.shadowRoot?.activeElement?.tagName, |
| 'GR-ACCOUNT-LIST' |
| ); |
| assert.equal(element?.shadowRoot?.activeElement?.id, 'reviewers'); |
| |
| element._focusOn(element.FocusTarget.CCS); |
| await flush(); |
| assert.equal(chooseFocusTargetSpy.callCount, 2); |
| assert.equal( |
| element?.shadowRoot?.activeElement?.tagName, |
| 'GR-ACCOUNT-LIST' |
| ); |
| assert.equal(element?.shadowRoot?.activeElement?.id, 'ccs'); |
| }); |
| |
| test('_chooseFocusTarget', () => { |
| element._account = undefined; |
| assert.strictEqual(element._chooseFocusTarget(), element.FocusTarget.BODY); |
| |
| element._account = {_account_id: 1 as AccountId}; |
| assert.strictEqual(element._chooseFocusTarget(), element.FocusTarget.BODY); |
| |
| element.change!.owner = {_account_id: 2 as AccountId}; |
| assert.strictEqual(element._chooseFocusTarget(), element.FocusTarget.BODY); |
| |
| element.change!.owner._account_id = 1 as AccountId; |
| assert.strictEqual( |
| element._chooseFocusTarget(), |
| element.FocusTarget.REVIEWERS |
| ); |
| |
| element._reviewers = []; |
| assert.strictEqual( |
| element._chooseFocusTarget(), |
| element.FocusTarget.REVIEWERS |
| ); |
| |
| element._reviewers.push({}); |
| assert.strictEqual(element._chooseFocusTarget(), element.FocusTarget.BODY); |
| }); |
| |
| test('only send labels that have changed', done => { |
| flush(() => { |
| stubSaveReview((review: ReviewInput) => { |
| assert.deepEqual(review?.labels, { |
| 'Code-Review': 0, |
| Verified: -1, |
| }); |
| }); |
| |
| element.addEventListener('send', () => { |
| done(); |
| }); |
| // Without wrapping this test in flush(), the below two calls to |
| // tap() cause a race in some situations in shadow DOM. |
| // The send button can be tapped before the others, causing the test to |
| // fail. |
| const el = queryAndAssert( |
| queryAndAssert(element, 'gr-label-scores'), |
| 'gr-label-score-row[name="Verified"]' |
| ) as GrLabelScoreRow; |
| el.setSelectedValue('-1'); |
| tap(queryAndAssert(element, '.send')); |
| }); |
| }); |
| |
| test('moving from cc to reviewer', () => { |
| flush(); |
| |
| const reviewer1 = makeAccount(); |
| const reviewer2 = makeAccount(); |
| const reviewer3 = makeAccount(); |
| const cc1 = makeAccount(); |
| const cc2 = makeAccount(); |
| const cc3 = makeAccount(); |
| const cc4 = makeAccount(); |
| element._reviewers = [reviewer1, reviewer2, reviewer3]; |
| element._ccs = [cc1, cc2, cc3, cc4]; |
| element.push('_reviewers', cc1); |
| flush(); |
| |
| assert.deepEqual(element._reviewers, [ |
| reviewer1, |
| reviewer2, |
| reviewer3, |
| cc1, |
| ]); |
| assert.deepEqual(element._ccs, [cc2, cc3, cc4]); |
| |
| element.push('_reviewers', cc4, cc3); |
| flush(); |
| |
| assert.deepEqual(element._reviewers, [ |
| reviewer1, |
| reviewer2, |
| reviewer3, |
| cc1, |
| cc4, |
| cc3, |
| ]); |
| assert.deepEqual(element._ccs, [cc2]); |
| }); |
| |
| test('update attention section when reviewers and ccs change', () => { |
| element._account = makeAccount(); |
| element._reviewers = [makeAccount(), makeAccount()]; |
| element._ccs = [makeAccount(), makeAccount()]; |
| element.draftCommentThreads = []; |
| const modifyButton = queryAndAssert(element, '.edit-attention-button'); |
| tap(modifyButton); |
| flush(); |
| |
| // "Modify" button disabled, because "Send" button is disabled. |
| assert.isFalse(element._attentionExpanded); |
| element.draft = 'a test comment'; |
| tap(modifyButton); |
| flush(); |
| assert.isTrue(element._attentionExpanded); |
| |
| let accountLabels = Array.from( |
| queryAll(element, '.attention-detail gr-account-label') |
| ); |
| assert.equal(accountLabels.length, 5); |
| |
| element.push('_reviewers', makeAccount()); |
| element.push('_ccs', makeAccount()); |
| flush(); |
| |
| // The 'attention modified' section collapses and resets when reviewers or |
| // ccs change. |
| assert.isFalse(element._attentionExpanded); |
| |
| tap(queryAndAssert(element, '.edit-attention-button')); |
| flush(); |
| |
| assert.isTrue(element._attentionExpanded); |
| accountLabels = Array.from( |
| queryAll(element, '.attention-detail gr-account-label') |
| ); |
| assert.equal(accountLabels.length, 7); |
| |
| element.pop('_reviewers'); |
| element.pop('_reviewers'); |
| element.pop('_ccs'); |
| element.pop('_ccs'); |
| |
| tap(queryAndAssert(element, '.edit-attention-button')); |
| flush(); |
| |
| accountLabels = Array.from( |
| queryAll(element, '.attention-detail gr-account-label') |
| ); |
| assert.equal(accountLabels.length, 3); |
| }); |
| |
| test('moving from reviewer to cc', () => { |
| flush(); |
| |
| const reviewer1 = makeAccount(); |
| const reviewer2 = makeAccount(); |
| const reviewer3 = makeAccount(); |
| const cc1 = makeAccount(); |
| const cc2 = makeAccount(); |
| const cc3 = makeAccount(); |
| const cc4 = makeAccount(); |
| element._reviewers = [reviewer1, reviewer2, reviewer3]; |
| element._ccs = [cc1, cc2, cc3, cc4]; |
| element.push('_ccs', reviewer1); |
| flush(); |
| |
| assert.deepEqual(element._reviewers, [reviewer2, reviewer3]); |
| assert.deepEqual(element._ccs, [cc1, cc2, cc3, cc4, reviewer1]); |
| |
| element.push('_ccs', reviewer3, reviewer2); |
| flush(); |
| |
| assert.deepEqual(element._reviewers, []); |
| assert.deepEqual(element._ccs, [ |
| cc1, |
| cc2, |
| cc3, |
| cc4, |
| reviewer1, |
| reviewer3, |
| reviewer2, |
| ]); |
| }); |
| |
| test('migrate reviewers between states', async () => { |
| flush(); |
| const reviewers = queryAndAssert(element, '#reviewers') as GrAccountList; |
| const ccs = queryAndAssert(element, '#ccs') as GrAccountList; |
| const reviewer1 = makeAccount(); |
| const reviewer2 = makeAccount(); |
| const cc1 = makeAccount(); |
| const cc2 = makeAccount(); |
| const cc3 = makeAccount(); |
| element._reviewers = [reviewer1, reviewer2]; |
| element._ccs = [cc1, cc2, cc3]; |
| |
| element.change!.reviewers = { |
| [ReviewerState.CC]: [], |
| [ReviewerState.REVIEWER]: [{_account_id: 33 as AccountId}], |
| }; |
| |
| const mutations: ReviewerInput[] = []; |
| |
| stubSaveReview((review: ReviewInput) => { |
| mutations.push(...review!.reviewers!); |
| }); |
| |
| // Remove and add to other field. |
| reviewers.dispatchEvent( |
| new CustomEvent('remove', { |
| detail: {account: reviewer1}, |
| composed: true, |
| bubbles: true, |
| }) |
| ); |
| ccs.$.entry.dispatchEvent( |
| new CustomEvent('add', { |
| detail: {value: {account: reviewer1}}, |
| composed: true, |
| bubbles: true, |
| }) |
| ); |
| ccs.dispatchEvent( |
| new CustomEvent('remove', { |
| detail: {account: cc1}, |
| composed: true, |
| bubbles: true, |
| }) |
| ); |
| ccs.dispatchEvent( |
| new CustomEvent('remove', { |
| detail: {account: cc3}, |
| composed: true, |
| bubbles: true, |
| }) |
| ); |
| reviewers.$.entry.dispatchEvent( |
| new CustomEvent('add', { |
| detail: {value: {account: cc1}}, |
| composed: true, |
| bubbles: true, |
| }) |
| ); |
| |
| // Add to other field without removing from former field. |
| // (Currently not possible in UI, but this is a good consistency check). |
| reviewers.$.entry.dispatchEvent( |
| new CustomEvent('add', { |
| detail: {value: {account: cc2}}, |
| composed: true, |
| bubbles: true, |
| }) |
| ); |
| ccs.$.entry.dispatchEvent( |
| new CustomEvent('add', { |
| detail: {value: {account: reviewer2}}, |
| composed: true, |
| bubbles: true, |
| }) |
| ); |
| |
| const mapReviewer = function ( |
| reviewer: AccountInfo, |
| opt_state?: ReviewerState |
| ) { |
| const result: ReviewerInput = { |
| reviewer: reviewer._account_id as AccountId, |
| }; |
| if (opt_state) { |
| result.state = opt_state; |
| } |
| return result; |
| }; |
| |
| // Send and purge and verify moves, delete cc3. |
| await element.send(false, false); |
| expect(mutations).to.have.lengthOf(5); |
| expect(mutations[0]).to.deep.equal( |
| mapReviewer(cc1, ReviewerState.REVIEWER) |
| ); |
| expect(mutations[1]).to.deep.equal( |
| mapReviewer(cc2, ReviewerState.REVIEWER) |
| ); |
| expect(mutations[2]).to.deep.equal( |
| mapReviewer(reviewer1, ReviewerState.CC) |
| ); |
| expect(mutations[3]).to.deep.equal( |
| mapReviewer(reviewer2, ReviewerState.CC) |
| ); |
| |
| // Only 1 account was initially part of the change |
| expect(mutations[4]).to.deep.equal({ |
| reviewer: 33, |
| state: ReviewerState.REMOVED, |
| }); |
| }); |
| |
| test('Ignore removal requests if being added as reviewer/CC', async () => { |
| flush(); |
| const reviewers = queryAndAssert(element, '#reviewers') as GrAccountList; |
| const ccs = queryAndAssert(element, '#ccs') as GrAccountList; |
| const reviewer1 = makeAccount(); |
| element._reviewers = [reviewer1]; |
| element._ccs = []; |
| |
| element.change!.reviewers = { |
| [ReviewerState.CC]: [], |
| [ReviewerState.REVIEWER]: [{_account_id: reviewer1._account_id}], |
| }; |
| |
| const mutations: ReviewerInput[] = []; |
| |
| stubSaveReview((review: ReviewInput) => { |
| mutations.push(...review!.reviewers!); |
| }); |
| |
| // Remove and add to other field. |
| reviewers.dispatchEvent( |
| new CustomEvent('remove', { |
| detail: {account: reviewer1}, |
| composed: true, |
| bubbles: true, |
| }) |
| ); |
| ccs.$.entry.dispatchEvent( |
| new CustomEvent('add', { |
| detail: {value: {account: reviewer1}}, |
| composed: true, |
| bubbles: true, |
| }) |
| ); |
| |
| await element.send(false, false); |
| expect(mutations).to.have.lengthOf(1); |
| // Only 1 account was initially part of the change |
| expect(mutations[0]).to.deep.equal({ |
| reviewer: reviewer1._account_id, |
| state: ReviewerState.CC, |
| }); |
| }); |
| |
| test('emits cancel on esc key', () => { |
| const cancelHandler = sinon.spy(); |
| element.addEventListener('cancel', cancelHandler); |
| pressAndReleaseKeyOn(element, 27, null, 'esc'); |
| flush(); |
| |
| assert.isTrue(cancelHandler.called); |
| }); |
| |
| test('should not send on enter key', () => { |
| stubSaveReview(() => undefined); |
| element.addEventListener('send', () => assert.fail('wrongly called')); |
| pressAndReleaseKeyOn(element, 13, null, 'enter'); |
| flush(); |
| }); |
| |
| test('emit send on ctrl+enter key', done => { |
| stubSaveReview(() => undefined); |
| element.addEventListener('send', () => done()); |
| pressAndReleaseKeyOn(element, 13, 'ctrl', 'enter'); |
| flush(); |
| }); |
| |
| test('_computeMessagePlaceholder', () => { |
| assert.equal( |
| element._computeMessagePlaceholder(false), |
| 'Say something nice...' |
| ); |
| assert.equal( |
| element._computeMessagePlaceholder(true), |
| 'Add a note for your reviewers...' |
| ); |
| }); |
| |
| test('_computeSendButtonLabel', () => { |
| assert.equal(element._computeSendButtonLabel(false), 'Send'); |
| assert.equal( |
| element._computeSendButtonLabel(true), |
| 'Send and Start review' |
| ); |
| }); |
| |
| test('_handle400Error reviewers and CCs', done => { |
| const error1 = 'error 1'; |
| const error2 = 'error 2'; |
| const error3 = 'error 3'; |
| const text = |
| ")]}'" + |
| JSON.stringify({ |
| reviewers: { |
| username1: { |
| input: 'username1', |
| error: error1, |
| }, |
| username2: { |
| input: 'username2', |
| error: error2, |
| }, |
| username3: { |
| input: 'username3', |
| error: error3, |
| }, |
| }, |
| }); |
| const listener = (e: Event) => { |
| (e as CustomEvent).detail.response.text().then((text: string) => { |
| assert.equal(text, [error1, error2, error3].join(', ')); |
| done(); |
| }); |
| }; |
| addListenerForTest(document, 'server-error', listener); |
| element._handle400Error(cloneableResponse(400, text) as Response); |
| }); |
| |
| test('fires height change when the drafts comments load', done => { |
| // Flush DOM operations before binding to the autogrow event so we don't |
| // catch the events fired from the initial layout. |
| flush(() => { |
| const autoGrowHandler = sinon.stub(); |
| element.addEventListener('autogrow', autoGrowHandler); |
| element.draftCommentThreads = []; |
| flush(() => { |
| assert.isTrue(autoGrowHandler.called); |
| done(); |
| }); |
| }); |
| }); |
| |
| suite('start review and save buttons', () => { |
| let sendStub: sinon.SinonStub; |
| |
| setup(() => { |
| sendStub = sinon.stub(element, 'send').callsFake(() => Promise.resolve()); |
| element.canBeStarted = true; |
| // Flush to make both Start/Save buttons appear in DOM. |
| flush(); |
| }); |
| |
| test('start review sets ready', () => { |
| tap(queryAndAssert(element, '.send')); |
| flush(); |
| assert.isTrue(sendStub.calledWith(true, true)); |
| }); |
| |
| test("save review doesn't set ready", () => { |
| tap(queryAndAssert(element, '.save')); |
| flush(); |
| assert.isTrue(sendStub.calledWith(true, false)); |
| }); |
| }); |
| |
| test('buttons disabled until all API calls are resolved', () => { |
| stubSaveReview(() => { |
| return {ready: true}; |
| }); |
| return element.send(true, true).then(() => { |
| assert.isFalse(element.disabled); |
| }); |
| }); |
| |
| suite('error handling', () => { |
| const expectedDraft = 'draft'; |
| const expectedError = new Error('test'); |
| |
| setup(() => { |
| element.draft = expectedDraft; |
| }); |
| |
| function assertDialogOpenAndEnabled() { |
| assert.strictEqual(expectedDraft, element.draft); |
| assert.isFalse(element.disabled); |
| } |
| |
| test('error occurs in _saveReview', () => { |
| stubSaveReview(() => { |
| throw expectedError; |
| }); |
| return element.send(true, true).catch(err => { |
| assert.strictEqual(expectedError, err); |
| assertDialogOpenAndEnabled(); |
| }); |
| }); |
| |
| suite('pending diff drafts?', () => { |
| test('yes', async () => { |
| const promise = mockPromise(); |
| const refreshSpy = sinon.spy(); |
| element.addEventListener('comment-refresh', refreshSpy); |
| stubRestApi('hasPendingDiffDrafts').returns(1); |
| stubRestApi('awaitPendingDiffDrafts').returns(promise as Promise<void>); |
| |
| element.open(); |
| |
| assert.isFalse(refreshSpy.called); |
| assert.isTrue(element._savingComments); |
| |
| promise.resolve(); |
| await flush(); |
| |
| assert.isTrue(refreshSpy.called); |
| assert.isFalse(element._savingComments); |
| }); |
| |
| test('no', () => { |
| stubRestApi('hasPendingDiffDrafts').returns(0); |
| element.open(); |
| assert.isFalse(element._savingComments); |
| }); |
| }); |
| }); |
| |
| test('_computeSendButtonDisabled_canBeStarted', () => { |
| // Mock canBeStarted |
| assert.isFalse( |
| element._computeSendButtonDisabled( |
| /* canBeStarted= */ true, |
| /* draftCommentThreads= */ [], |
| /* text= */ '', |
| /* reviewersMutated= */ false, |
| /* labelsChanged= */ false, |
| /* includeComments= */ false, |
| /* disabled= */ false, |
| /* commentEditing= */ false, |
| /* change= */ element.change, |
| /* account= */ makeAccount() |
| ) |
| ); |
| }); |
| |
| test('_computeSendButtonDisabled_allFalse', () => { |
| // Mock everything false |
| assert.isTrue( |
| element._computeSendButtonDisabled( |
| /* canBeStarted= */ false, |
| /* draftCommentThreads= */ [], |
| /* text= */ '', |
| /* reviewersMutated= */ false, |
| /* labelsChanged= */ false, |
| /* includeComments= */ false, |
| /* disabled= */ false, |
| /* commentEditing= */ false, |
| /* change= */ element.change, |
| /* account= */ makeAccount() |
| ) |
| ); |
| }); |
| |
| test('_computeSendButtonDisabled_draftCommentsSend', () => { |
| // Mock nonempty comment draft array, with sending comments. |
| assert.isFalse( |
| element._computeSendButtonDisabled( |
| /* canBeStarted= */ false, |
| /* draftCommentThreads= */ [ |
| {...createCommentThread([{__draft: true}])}, |
| ], |
| /* text= */ '', |
| /* reviewersMutated= */ false, |
| /* labelsChanged= */ false, |
| /* includeComments= */ true, |
| /* disabled= */ false, |
| /* commentEditing= */ false, |
| /* change= */ element.change, |
| /* account= */ makeAccount() |
| ) |
| ); |
| }); |
| |
| test('_computeSendButtonDisabled_draftCommentsDoNotSend', () => { |
| // Mock nonempty comment draft array, without sending comments. |
| assert.isTrue( |
| element._computeSendButtonDisabled( |
| /* canBeStarted= */ false, |
| /* draftCommentThreads= */ [ |
| {...createCommentThread([{__draft: true}])}, |
| ], |
| /* text= */ '', |
| /* reviewersMutated= */ false, |
| /* labelsChanged= */ false, |
| /* includeComments= */ false, |
| /* disabled= */ false, |
| /* commentEditing= */ false, |
| /* change= */ element.change, |
| /* account= */ makeAccount() |
| ) |
| ); |
| }); |
| |
| test('_computeSendButtonDisabled_changeMessage', () => { |
| // Mock nonempty change message. |
| assert.isFalse( |
| element._computeSendButtonDisabled( |
| /* canBeStarted= */ false, |
| /* draftCommentThreads= */ [{...createCommentThread([{}])}], |
| /* text= */ 'test', |
| /* reviewersMutated= */ false, |
| /* labelsChanged= */ false, |
| /* includeComments= */ false, |
| /* disabled= */ false, |
| /* commentEditing= */ false, |
| /* change= */ element.change, |
| /* account= */ makeAccount() |
| ) |
| ); |
| }); |
| |
| test('_computeSendButtonDisabled_reviewersChanged', () => { |
| // Mock reviewers mutated. |
| assert.isFalse( |
| element._computeSendButtonDisabled( |
| /* canBeStarted= */ false, |
| /* draftCommentThreads= */ [{...createCommentThread([{}])}], |
| /* text= */ '', |
| /* reviewersMutated= */ true, |
| /* labelsChanged= */ false, |
| /* includeComments= */ false, |
| /* disabled= */ false, |
| /* commentEditing= */ false, |
| /* change= */ element.change, |
| /* account= */ makeAccount() |
| ) |
| ); |
| }); |
| |
| test('_computeSendButtonDisabled_labelsChanged', () => { |
| // Mock labels changed. |
| assert.isFalse( |
| element._computeSendButtonDisabled( |
| /* canBeStarted= */ false, |
| /* draftCommentThreads= */ [{...createCommentThread([{}])}], |
| /* text= */ '', |
| /* reviewersMutated= */ false, |
| /* labelsChanged= */ true, |
| /* includeComments= */ false, |
| /* disabled= */ false, |
| /* commentEditing= */ false, |
| /* change= */ element.change, |
| /* account= */ makeAccount() |
| ) |
| ); |
| }); |
| |
| test('_computeSendButtonDisabled_dialogDisabled', () => { |
| // Whole dialog is disabled. |
| assert.isTrue( |
| element._computeSendButtonDisabled( |
| /* canBeStarted= */ false, |
| /* draftCommentThreads= */ [{...createCommentThread([{}])}], |
| /* text= */ '', |
| /* reviewersMutated= */ false, |
| /* labelsChanged= */ true, |
| /* includeComments= */ false, |
| /* disabled= */ true, |
| /* commentEditing= */ false, |
| /* change= */ element.change, |
| /* account= */ makeAccount() |
| ) |
| ); |
| }); |
| |
| test('_computeSendButtonDisabled_existingVote', async () => { |
| const account = createAccountWithId(); |
| (element.change!.labels![CODE_REVIEW]! as DetailedLabelInfo).all = [ |
| account, |
| ]; |
| await flush(); |
| |
| // User has already voted. |
| assert.isFalse( |
| element._computeSendButtonDisabled( |
| /* canBeStarted= */ false, |
| /* draftCommentThreads= */ [{...createCommentThread([{}])}], |
| /* text= */ '', |
| /* reviewersMutated= */ false, |
| /* labelsChanged= */ false, |
| /* includeComments= */ false, |
| /* disabled= */ false, |
| /* commentEditing= */ false, |
| /* change= */ element.change, |
| /* account= */ account |
| ) |
| ); |
| }); |
| |
| test('_submit blocked when no mutations exist', async () => { |
| const sendStub = sinon.stub(element, 'send').returns(Promise.resolve()); |
| element.draftCommentThreads = []; |
| await flush(); |
| |
| tap(queryAndAssert(element, 'gr-button.send')); |
| assert.isFalse(sendStub.called); |
| |
| element.draftCommentThreads = [ |
| { |
| ...createCommentThread([ |
| {__draft: true, path: 'test', line: 1, patch_set: 1 as PatchSetNum}, |
| ]), |
| }, |
| ]; |
| await flush(); |
| |
| tap(queryAndAssert(element, 'gr-button.send')); |
| assert.isTrue(sendStub.called); |
| }); |
| |
| test('getFocusStops', async () => { |
| // Setting draftCommentThreads to an empty object causes _sendDisabled to be |
| // computed to false. |
| element.draftCommentThreads = []; |
| await flush(); |
| |
| assert.equal( |
| element.getFocusStops().end, |
| queryAndAssert(element, '#cancelButton') |
| ); |
| element.draftCommentThreads = [ |
| { |
| ...createCommentThread([ |
| {__draft: true, path: 'test', line: 1, patch_set: 1 as PatchSetNum}, |
| ]), |
| }, |
| ]; |
| await flush(); |
| |
| assert.equal( |
| element.getFocusStops().end, |
| queryAndAssert(element, '#sendButton') |
| ); |
| }); |
| |
| test('setPluginMessage', () => { |
| element.setPluginMessage('foo'); |
| assert.equal(queryAndAssert(element, '#pluginMessage').textContent, 'foo'); |
| }); |
| }); |