| /** |
| * @license |
| * Copyright 2015 Google LLC |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| import '../../../test/common-test-setup'; |
| import '../../edit/gr-edit-constants'; |
| import '../gr-thread-list/gr-thread-list'; |
| import './gr-change-view'; |
| import { |
| ChangeStatus, |
| CommentSide, |
| DefaultBase, |
| DiffViewMode, |
| HttpMethod, |
| MessageTag, |
| createDefaultPreferences, |
| Tab, |
| } from '../../../constants/constants'; |
| import {GrEditConstants} from '../../edit/gr-edit-constants'; |
| import {_testOnly_resetEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints'; |
| import {navigationToken} from '../../core/gr-navigation/gr-navigation'; |
| import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader'; |
| import {EventType, PluginApi} from '../../../api/plugin'; |
| import { |
| mockPromise, |
| pressKey, |
| queryAndAssert, |
| stubBaseUrl, |
| stubFlags, |
| stubRestApi, |
| stubUsers, |
| waitEventLoop, |
| waitQueryAndAssert, |
| waitUntil, |
| } from '../../../test/test-utils'; |
| import { |
| createChangeViewState, |
| createApproval, |
| createChange, |
| createChangeMessages, |
| createCommit, |
| createMergeable, |
| createPreferences, |
| createRevision, |
| createRevisions, |
| createServerInfo, |
| createUserConfig, |
| TEST_NUMERIC_CHANGE_ID, |
| TEST_PROJECT_NAME, |
| createEditRevision, |
| createAccountWithIdNameAndEmail, |
| createChangeViewChange, |
| createRelatedChangeAndCommitInfo, |
| createAccountDetailWithId, |
| createParsedChange, |
| createDraft, |
| } from '../../../test/test-data-generators'; |
| import {GrChangeView} from './gr-change-view'; |
| import { |
| AccountId, |
| ApprovalInfo, |
| BasePatchSetNum, |
| ChangeId, |
| ChangeInfo, |
| CommitId, |
| EDIT, |
| NumericChangeId, |
| PARENT, |
| RelatedChangeAndCommitInfo, |
| ReviewInputTag, |
| RevisionInfo, |
| RevisionPatchSetNum, |
| RobotId, |
| RobotCommentInfo, |
| Timestamp, |
| UrlEncodedCommentId, |
| DetailedLabelInfo, |
| RepoName, |
| QuickLabelInfo, |
| } from '../../../types/common'; |
| import {GrEditControls} from '../../edit/gr-edit-controls/gr-edit-controls'; |
| import {SinonFakeTimers, SinonStubbedMember} from 'sinon'; |
| import {RestApiService} from '../../../services/gr-rest-api/gr-rest-api'; |
| import {CommentThread} from '../../../utils/comment-util'; |
| import {GerritView} from '../../../services/router/router-model'; |
| import {ParsedChangeInfo} from '../../../types/types'; |
| import {GrRelatedChangesList} from '../gr-related-changes-list/gr-related-changes-list'; |
| import {ChangeStates} from '../../shared/gr-change-status/gr-change-status'; |
| import {LoadingStatus} from '../../../models/change/change-model'; |
| import {FocusTarget, GrReplyDialog} from '../gr-reply-dialog/gr-reply-dialog'; |
| import {GrOverlay} from '../../shared/gr-overlay/gr-overlay'; |
| import {GrChangeStar} from '../../shared/gr-change-star/gr-change-star'; |
| import {GrThreadList} from '../gr-thread-list/gr-thread-list'; |
| import {assertIsDefined} from '../../../utils/common-util'; |
| import {DEFAULT_NUM_FILES_SHOWN} from '../gr-file-list/gr-file-list'; |
| import {fixture, html, assert} from '@open-wc/testing'; |
| import {deepClone} from '../../../utils/deep-util'; |
| import {Modifier} from '../../../utils/dom-util'; |
| import {GrButton} from '../../shared/gr-button/gr-button'; |
| import {GrCopyLinks} from '../gr-copy-links/gr-copy-links'; |
| import {ChangeViewState} from '../../../models/views/change'; |
| import {rootUrl} from '../../../utils/url-util'; |
| import {testResolver} from '../../../test/common-test-setup'; |
| |
| suite('gr-change-view tests', () => { |
| let element: GrChangeView; |
| let setUrlStub: sinon.SinonStub; |
| |
| const ROBOT_COMMENTS_LIMIT = 10; |
| |
| // TODO: should have a mock service to generate VALID fake data |
| const THREADS: CommentThread[] = [ |
| { |
| comments: [ |
| { |
| path: '/COMMIT_MSG', |
| author: { |
| _account_id: 1000000 as AccountId, |
| name: 'user', |
| username: 'user', |
| }, |
| patch_set: 2 as RevisionPatchSetNum, |
| robot_id: 'rb1' as RobotId, |
| id: 'ecf0b9fa_fe1a5f62' as UrlEncodedCommentId, |
| line: 5, |
| updated: '2018-02-08 18:49:18.000000000' as Timestamp, |
| message: 'test', |
| unresolved: true, |
| }, |
| { |
| path: '/COMMIT_MSG', |
| author: { |
| _account_id: 1000000 as AccountId, |
| name: 'user', |
| username: 'user', |
| }, |
| patch_set: 4 as RevisionPatchSetNum, |
| id: 'ecf0b9fa_fe1a5f62_1' as UrlEncodedCommentId, |
| line: 5, |
| updated: '2018-02-08 18:49:18.000000000' as Timestamp, |
| message: 'test', |
| unresolved: true, |
| }, |
| { |
| id: '503008e2_0ab203ee' as UrlEncodedCommentId, |
| path: '/COMMIT_MSG', |
| line: 5, |
| in_reply_to: 'ecf0b9fa_fe1a5f62' as UrlEncodedCommentId, |
| updated: '2018-02-13 22:48:48.018000000' as Timestamp, |
| message: 'draft', |
| unresolved: false, |
| __draft: true, |
| patch_set: 2 as RevisionPatchSetNum, |
| }, |
| ], |
| patchNum: 4 as RevisionPatchSetNum, |
| path: '/COMMIT_MSG', |
| line: 5, |
| rootId: 'ecf0b9fa_fe1a5f62' as UrlEncodedCommentId, |
| commentSide: CommentSide.REVISION, |
| }, |
| { |
| comments: [ |
| { |
| path: '/COMMIT_MSG', |
| author: { |
| _account_id: 1000000 as AccountId, |
| name: 'user', |
| username: 'user', |
| }, |
| patch_set: 3 as RevisionPatchSetNum, |
| id: 'ecf0b9fa_fe5f62' as UrlEncodedCommentId, |
| robot_id: 'rb2' as RobotId, |
| line: 5, |
| updated: '2018-02-08 18:49:18.000000000' as Timestamp, |
| message: 'test', |
| unresolved: true, |
| }, |
| { |
| path: 'test.txt', |
| author: { |
| _account_id: 1000000 as AccountId, |
| name: 'user', |
| username: 'user', |
| }, |
| patch_set: 3 as RevisionPatchSetNum, |
| id: '09a9fb0a_1484e6cf' as UrlEncodedCommentId, |
| side: CommentSide.PARENT, |
| updated: '2018-02-13 22:47:19.000000000' as Timestamp, |
| message: 'Some comment on another patchset.', |
| unresolved: false, |
| }, |
| ], |
| patchNum: 3 as RevisionPatchSetNum, |
| path: 'test.txt', |
| rootId: '09a9fb0a_1484e6cf' as UrlEncodedCommentId, |
| commentSide: CommentSide.PARENT, |
| }, |
| { |
| comments: [ |
| { |
| path: '/COMMIT_MSG', |
| author: { |
| _account_id: 1000000 as AccountId, |
| name: 'user', |
| username: 'user', |
| }, |
| patch_set: 2 as RevisionPatchSetNum, |
| id: '8caddf38_44770ec1' as UrlEncodedCommentId, |
| line: 4, |
| updated: '2018-02-13 22:48:40.000000000' as Timestamp, |
| message: 'Another unresolved comment', |
| unresolved: true, |
| }, |
| ], |
| patchNum: 2 as RevisionPatchSetNum, |
| path: '/COMMIT_MSG', |
| line: 4, |
| rootId: '8caddf38_44770ec1' as UrlEncodedCommentId, |
| commentSide: CommentSide.REVISION, |
| }, |
| { |
| comments: [ |
| { |
| path: '/COMMIT_MSG', |
| author: { |
| // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion |
| _account_id: 1000000 as AccountId, |
| name: 'user', |
| username: 'user', |
| }, |
| patch_set: 2 as RevisionPatchSetNum, |
| id: 'scaddf38_44770ec1' as UrlEncodedCommentId, |
| line: 4, |
| updated: '2018-02-14 22:48:40.000000000' as Timestamp, |
| message: 'Yet another unresolved comment', |
| unresolved: true, |
| }, |
| ], |
| patchNum: 2 as RevisionPatchSetNum, |
| path: '/COMMIT_MSG', |
| line: 4, |
| rootId: 'scaddf38_44770ec1' as UrlEncodedCommentId, |
| commentSide: CommentSide.REVISION, |
| }, |
| { |
| comments: [ |
| { |
| id: 'zcf0b9fa_fe1a5f62' as UrlEncodedCommentId, |
| path: '/COMMIT_MSG', |
| line: 6, |
| updated: '2018-02-15 22:48:48.018000000' as Timestamp, |
| message: 'resolved draft', |
| unresolved: false, |
| __draft: true, |
| patch_set: 2 as RevisionPatchSetNum, |
| }, |
| ], |
| patchNum: 4 as RevisionPatchSetNum, |
| path: '/COMMIT_MSG', |
| line: 6, |
| rootId: 'zcf0b9fa_fe1a5f62' as UrlEncodedCommentId, |
| commentSide: CommentSide.REVISION, |
| }, |
| { |
| comments: [ |
| { |
| path: '/COMMIT_MSG', |
| author: { |
| _account_id: 1000000 as AccountId, |
| name: 'user', |
| username: 'user', |
| }, |
| patch_set: 4 as RevisionPatchSetNum, |
| id: 'rc1' as UrlEncodedCommentId, |
| line: 5, |
| updated: '2019-02-08 18:49:18.000000000' as Timestamp, |
| message: 'test', |
| unresolved: true, |
| robot_id: 'rc1' as RobotId, |
| }, |
| ], |
| patchNum: 4 as RevisionPatchSetNum, |
| path: '/COMMIT_MSG', |
| line: 5, |
| rootId: 'rc1' as UrlEncodedCommentId, |
| commentSide: CommentSide.REVISION, |
| }, |
| { |
| comments: [ |
| { |
| path: '/COMMIT_MSG', |
| author: { |
| _account_id: 1000000 as AccountId, |
| name: 'user', |
| username: 'user', |
| }, |
| patch_set: 4 as RevisionPatchSetNum, |
| id: 'rc2' as UrlEncodedCommentId, |
| line: 5, |
| updated: '2019-03-08 18:49:18.000000000' as Timestamp, |
| message: 'test', |
| unresolved: true, |
| robot_id: 'rc2' as RobotId, |
| }, |
| { |
| path: '/COMMIT_MSG', |
| author: { |
| _account_id: 1000000 as AccountId, |
| name: 'user', |
| username: 'user', |
| }, |
| patch_set: 4 as RevisionPatchSetNum, |
| id: 'c2_1' as UrlEncodedCommentId, |
| line: 5, |
| updated: '2019-03-08 18:49:18.000000000' as Timestamp, |
| message: 'test', |
| unresolved: true, |
| }, |
| ], |
| patchNum: 4 as RevisionPatchSetNum, |
| path: '/COMMIT_MSG', |
| line: 5, |
| rootId: 'rc2' as UrlEncodedCommentId, |
| commentSide: CommentSide.REVISION, |
| }, |
| ]; |
| |
| setup(async () => { |
| // Since pluginEndpoints are global, must reset state. |
| _testOnly_resetEndpoints(); |
| setUrlStub = sinon.stub(testResolver(navigationToken), 'setUrl'); |
| |
| stubRestApi('getConfig').returns( |
| Promise.resolve({ |
| ...createServerInfo(), |
| user: { |
| ...createUserConfig(), |
| anonymous_coward_name: 'test coward name', |
| }, |
| }) |
| ); |
| stubRestApi('getAccount').returns( |
| Promise.resolve(createAccountDetailWithId(5)) |
| ); |
| stubRestApi('getDiffComments').returns(Promise.resolve({})); |
| stubRestApi('getDiffRobotComments').returns(Promise.resolve({})); |
| stubRestApi('getDiffDrafts').returns(Promise.resolve({})); |
| |
| getPluginLoader().loadPlugins([]); |
| window.Gerrit.install( |
| plugin => { |
| plugin.registerDynamicCustomComponent( |
| 'change-view-tab-header', |
| 'gr-checks-change-view-tab-header-view' |
| ); |
| plugin.registerDynamicCustomComponent( |
| 'change-view-tab-content', |
| 'gr-checks-view' |
| ); |
| }, |
| '0.1', |
| 'http://some/plugins/url.js' |
| ); |
| element = await fixture<GrChangeView>( |
| html`<gr-change-view></gr-change-view>` |
| ); |
| element.viewState = { |
| view: GerritView.CHANGE, |
| changeNum: TEST_NUMERIC_CHANGE_ID, |
| project: 'gerrit' as RepoName, |
| }; |
| await element.updateComplete.then(() => { |
| assertIsDefined(element.actions); |
| sinon.stub(element.actions, 'reload').returns(Promise.resolve()); |
| }); |
| }); |
| |
| teardown(async () => { |
| await element.updateComplete; |
| }); |
| |
| test('render', () => { |
| assert.shadowDom.equal( |
| element, |
| /* HTML */ ` |
| <div class="container loading">Loading...</div> |
| <div aria-hidden="false" class="container" hidden="" id="mainContent"> |
| <section class="changeInfoSection"> |
| <div class="header"> |
| <h1 class="assistive-tech-only">Change :</h1> |
| <div class="headerTitle"> |
| <div class="changeStatuses"></div> |
| <gr-button |
| aria-disabled="false" |
| class="showCopyLinkDialogButton" |
| down-arrow="" |
| flatten="" |
| role="button" |
| tabindex="0" |
| ><gr-change-star id="changeStar"> </gr-change-star> |
| <a aria-label="Change undefined" class="changeNumber"> </a> |
| </gr-button> |
| <span class="headerSubject"> </span> |
| <gr-copy-clipboard |
| class="changeCopyClipboard" |
| hideinput="" |
| text="undefined: undefined | http://localhost:9876undefined" |
| > |
| </gr-copy-clipboard> |
| </div> |
| <div class="commitActions"> |
| <gr-change-actions hidden="" id="actions"> </gr-change-actions> |
| </div> |
| </div> |
| <h2 class="assistive-tech-only">Change metadata</h2> |
| <div class="changeInfo"> |
| <div class="changeInfo-column changeMetadata hideOnMobileOverlay"> |
| <gr-change-metadata id="metadata"> </gr-change-metadata> |
| </div> |
| <div class="changeInfo-column mainChangeInfo" id="mainChangeInfo"> |
| <div class="hideOnMobileOverlay" id="commitAndRelated"> |
| <div class="commitContainer"> |
| <h3 class="assistive-tech-only">Commit Message</h3> |
| <div> |
| <gr-button |
| aria-disabled="false" |
| class="reply" |
| id="replyBtn" |
| primary="" |
| role="button" |
| tabindex="0" |
| title="Open reply dialog to publish comments and add reviewers (shortcut: a)" |
| > |
| Reply |
| </gr-button> |
| </div> |
| <div class="commitMessage" id="commitMessage"> |
| <gr-editable-content |
| id="commitMessageEditor" |
| remove-zero-width-space="" |
| > |
| <gr-formatted-text></gr-formatted-text> |
| </gr-editable-content> |
| <div class="changeId" hidden=""> |
| <hr /> |
| Change-Id: |
| <span class="" title=""></span> |
| </div> |
| </div> |
| <h3 class="assistive-tech-only"> |
| Comments and Checks Summary |
| </h3> |
| <gr-change-summary> </gr-change-summary> |
| <gr-endpoint-decorator name="commit-container"> |
| <gr-endpoint-param name="change"> </gr-endpoint-param> |
| <gr-endpoint-param name="revision"> </gr-endpoint-param> |
| </gr-endpoint-decorator> |
| </div> |
| <div class="relatedChanges"> |
| <gr-related-changes-list id="relatedChanges"> |
| </gr-related-changes-list> |
| </div> |
| <div class="emptySpace"></div> |
| </div> |
| </div> |
| </div> |
| </section> |
| <h2 class="assistive-tech-only">Files and Comments tabs</h2> |
| <paper-tabs dir="null" id="tabs" role="tablist" tabindex="0"> |
| <paper-tab |
| aria-disabled="false" |
| aria-selected="true" |
| class="iron-selected" |
| data-name="files" |
| role="tab" |
| tabindex="0" |
| > |
| <span> Files </span> |
| </paper-tab> |
| <paper-tab |
| aria-disabled="false" |
| aria-selected="false" |
| class="commentThreads" |
| data-name="comments" |
| role="tab" |
| tabindex="-1" |
| > |
| <gr-tooltip-content has-tooltip="" title=""> |
| <span> Comments </span> |
| </gr-tooltip-content> |
| </paper-tab> |
| <paper-tab |
| aria-disabled="false" |
| aria-selected="false" |
| data-name="change-view-tab-header-url" |
| role="tab" |
| tabindex="-1" |
| > |
| <gr-endpoint-decorator name="change-view-tab-header-url"> |
| <gr-endpoint-param name="change"> </gr-endpoint-param> |
| <gr-endpoint-param name="revision"> </gr-endpoint-param> |
| </gr-endpoint-decorator> |
| </paper-tab> |
| </paper-tabs> |
| <section class="tabContent"> |
| <div> |
| <gr-file-list-header id="fileListHeader"> </gr-file-list-header> |
| <gr-file-list class="hideOnMobileOverlay" id="fileList"> |
| </gr-file-list> |
| </div> |
| </section> |
| <gr-endpoint-decorator name="change-view-integration"> |
| <gr-endpoint-param name="change"> </gr-endpoint-param> |
| <gr-endpoint-param name="revision"> </gr-endpoint-param> |
| </gr-endpoint-decorator> |
| <paper-tabs dir="null" role="tablist" tabindex="0"> |
| <paper-tab |
| aria-disabled="false" |
| aria-selected="false" |
| class="changeLog" |
| data-name="_changeLog" |
| role="tab" |
| tabindex="-1" |
| > |
| Change Log |
| </paper-tab> |
| </paper-tabs> |
| <section class="changeLog"> |
| <h2 class="assistive-tech-only">Change Log</h2> |
| <gr-messages-list class="hideOnMobileOverlay"> </gr-messages-list> |
| </section> |
| </div> |
| <gr-apply-fix-dialog id="applyFixDialog"> </gr-apply-fix-dialog> |
| <gr-overlay |
| aria-hidden="true" |
| id="downloadOverlay" |
| style="outline: none; display: none;" |
| tabindex="-1" |
| with-backdrop="" |
| > |
| <gr-download-dialog id="downloadDialog" role="dialog"> |
| </gr-download-dialog> |
| </gr-overlay> |
| <gr-overlay |
| aria-hidden="true" |
| id="includedInOverlay" |
| style="outline: none; display: none;" |
| tabindex="-1" |
| with-backdrop="" |
| > |
| <gr-included-in-dialog id="includedInDialog"> </gr-included-in-dialog> |
| </gr-overlay> |
| <gr-overlay |
| aria-hidden="true" |
| class="scrollable" |
| id="replyOverlay" |
| no-cancel-on-esc-key="" |
| no-cancel-on-outside-click="" |
| scroll-action="lock" |
| style="outline: none; display: none;" |
| tabindex="-1" |
| with-backdrop="" |
| > |
| </gr-overlay> |
| ` |
| ); |
| }); |
| |
| test('handleMessageAnchorTap', async () => { |
| element.changeNum = 1 as NumericChangeId; |
| element.patchRange = { |
| basePatchNum: PARENT, |
| patchNum: 1 as RevisionPatchSetNum, |
| }; |
| element.change = createChangeViewChange(); |
| await element.updateComplete; |
| const replaceStateStub = sinon.stub(history, 'replaceState'); |
| element.handleMessageAnchorTap( |
| new CustomEvent('message-anchor-tap', {detail: {id: 'a12345'}}) |
| ); |
| |
| assert.isTrue(replaceStateStub.called); |
| }); |
| |
| test('handleDiffAgainstBase', () => { |
| element.change = { |
| ...createChangeViewChange(), |
| revisions: createRevisions(10), |
| }; |
| element.patchRange = { |
| patchNum: 3 as RevisionPatchSetNum, |
| basePatchNum: 1 as BasePatchSetNum, |
| }; |
| element.handleDiffAgainstBase(); |
| assert.isTrue(setUrlStub.called); |
| assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42/3'); |
| }); |
| |
| test('handleDiffAgainstLatest', () => { |
| element.change = { |
| ...createChangeViewChange(), |
| revisions: createRevisions(10), |
| }; |
| element.patchRange = { |
| basePatchNum: 1 as BasePatchSetNum, |
| patchNum: 3 as RevisionPatchSetNum, |
| }; |
| element.handleDiffAgainstLatest(); |
| assert.isTrue(setUrlStub.called); |
| assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42/1..10'); |
| }); |
| |
| test('handleDiffBaseAgainstLeft', () => { |
| element.change = { |
| ...createChangeViewChange(), |
| revisions: createRevisions(10), |
| }; |
| element.patchRange = { |
| patchNum: 3 as RevisionPatchSetNum, |
| basePatchNum: 1 as BasePatchSetNum, |
| }; |
| element.handleDiffBaseAgainstLeft(); |
| assert.isTrue(setUrlStub.called); |
| assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42/1'); |
| }); |
| |
| test('handleDiffRightAgainstLatest', () => { |
| element.change = { |
| ...createChangeViewChange(), |
| revisions: createRevisions(10), |
| }; |
| element.patchRange = { |
| basePatchNum: 1 as BasePatchSetNum, |
| patchNum: 3 as RevisionPatchSetNum, |
| }; |
| element.handleDiffRightAgainstLatest(); |
| assert.isTrue(setUrlStub.called); |
| assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42/3..10'); |
| }); |
| |
| test('handleDiffBaseAgainstLatest', () => { |
| element.change = { |
| ...createChangeViewChange(), |
| revisions: createRevisions(10), |
| }; |
| element.patchRange = { |
| basePatchNum: 1 as BasePatchSetNum, |
| patchNum: 3 as RevisionPatchSetNum, |
| }; |
| element.handleDiffBaseAgainstLatest(); |
| assert.isTrue(setUrlStub.called); |
| assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42/10'); |
| }); |
| |
| test('toggle attention set status', async () => { |
| element.change = { |
| ...createChangeViewChange(), |
| revisions: createRevisions(10), |
| }; |
| const addToAttentionSetStub = stubRestApi('addToAttentionSet').returns( |
| Promise.resolve(new Response()) |
| ); |
| |
| const removeFromAttentionSetStub = stubRestApi( |
| 'removeFromAttentionSet' |
| ).returns(Promise.resolve(new Response())); |
| element.patchRange = { |
| basePatchNum: 1 as BasePatchSetNum, |
| patchNum: 3 as RevisionPatchSetNum, |
| }; |
| await element.updateComplete; |
| assert.isNotOk(element.change.attention_set); |
| element.handleToggleAttentionSet(); |
| assert.isTrue(addToAttentionSetStub.called); |
| assert.isFalse(removeFromAttentionSetStub.called); |
| |
| element.handleToggleAttentionSet(); |
| assert.isTrue(removeFromAttentionSetStub.called); |
| }); |
| |
| suite('plugins adding to file tab', () => { |
| setup(async () => { |
| element.changeNum = TEST_NUMERIC_CHANGE_ID; |
| await element.updateComplete; |
| await waitUntil(() => element.pluginTabsHeaderEndpoints.length > 0); |
| }); |
| |
| test('plugin added tab shows up as a dynamic endpoint', async () => { |
| assert( |
| element.pluginTabsHeaderEndpoints.includes('change-view-tab-header-url') |
| ); |
| const tabs = element.shadowRoot!.querySelector('#tabs')!; |
| const paperTabs = tabs.querySelectorAll<HTMLElement>('paper-tab'); |
| // 4 Tabs are : Files, Comment Threads, Plugin |
| assert.equal(tabs.querySelectorAll('paper-tab').length, 3); |
| assert.equal(paperTabs[0].dataset.name, 'files'); |
| assert.equal(paperTabs[1].dataset.name, 'comments'); |
| assert.equal(paperTabs[2].dataset.name, 'change-view-tab-header-url'); |
| }); |
| |
| test('setActiveTab switched tab correctly', async () => { |
| element.setActiveTab( |
| new CustomEvent('', { |
| detail: {tab: 'change-view-tab-header-url'}, |
| }) |
| ); |
| await element.updateComplete; |
| assert.equal(element.activeTab, 'change-view-tab-header-url'); |
| }); |
| |
| test('show-tab switched primary tab correctly', async () => { |
| element.dispatchEvent( |
| new CustomEvent('show-tab', { |
| composed: true, |
| bubbles: true, |
| detail: { |
| tab: 'change-view-tab-header-url', |
| }, |
| }) |
| ); |
| await element.updateComplete; |
| assert.equal(element.activeTab, 'change-view-tab-header-url'); |
| }); |
| |
| test('param change should switch primary tab correctly', async () => { |
| assert.equal(element.activeTab, Tab.FILES); |
| // view is required |
| element.changeNum = undefined; |
| element.viewState = { |
| ...createChangeViewState(), |
| ...element.viewState, |
| tab: Tab.COMMENT_THREADS, |
| }; |
| await element.updateComplete; |
| assert.equal(element.activeTab, Tab.COMMENT_THREADS); |
| }); |
| |
| test('invalid param change should not switch primary tab', async () => { |
| assert.equal(element.activeTab, Tab.FILES); |
| // view is required |
| element.viewState = { |
| ...createChangeViewState(), |
| ...element.viewState, |
| tab: 'random', |
| }; |
| await element.updateComplete; |
| assert.equal(element.activeTab, Tab.FILES); |
| }); |
| |
| test('switching to plugin tab renders the plugin tab content', async () => { |
| const paperTabs = element.shadowRoot!.querySelector('#tabs')!; |
| paperTabs.querySelectorAll('paper-tab')[2].click(); |
| await element.updateComplete; |
| const tabContent = queryAndAssert(element, '.tabContent'); |
| const endpoint = queryAndAssert(tabContent, 'gr-endpoint-decorator'); |
| assert.dom.equal( |
| endpoint, |
| /* HTML */ ` |
| <gr-endpoint-decorator> |
| <gr-endpoint-param name="change"></gr-endpoint-param> |
| <gr-endpoint-param name="revision"></gr-endpoint-param> |
| </gr-endpoint-decorator> |
| ` |
| ); |
| }); |
| }); |
| |
| suite('keyboard shortcuts', () => { |
| let clock: SinonFakeTimers; |
| setup(() => { |
| clock = sinon.useFakeTimers(); |
| }); |
| |
| teardown(() => { |
| clock.restore(); |
| sinon.restore(); |
| }); |
| |
| test('t to add topic', () => { |
| assertIsDefined(element.metadata); |
| const editStub = sinon.stub(element.metadata, 'editTopic'); |
| pressKey(element, 't'); |
| assert(editStub.called); |
| }); |
| |
| test('S should toggle the CL star', () => { |
| assertIsDefined(element.changeStar); |
| const starStub = sinon.stub(element.changeStar, 'toggleStar'); |
| pressKey(element, 's'); |
| assert(starStub.called); |
| }); |
| |
| test('toggle star is throttled', () => { |
| assertIsDefined(element.changeStar); |
| const starStub = sinon.stub(element.changeStar, 'toggleStar'); |
| pressKey(element, 's'); |
| assert(starStub.called); |
| pressKey(element, 's'); |
| assert.equal(starStub.callCount, 1); |
| clock.tick(1000); |
| pressKey(element, 's'); |
| assert.equal(starStub.callCount, 2); |
| }); |
| |
| test('U should navigate to root if no backPage set', () => { |
| pressKey(element, 'u'); |
| assert.isTrue(setUrlStub.called); |
| assert.isTrue(setUrlStub.lastCall.calledWithExactly(rootUrl())); |
| }); |
| |
| test('U should navigate to backPage if set', () => { |
| element.backPage = '/dashboard/self'; |
| pressKey(element, 'u'); |
| assert.isTrue(setUrlStub.called); |
| assert.isTrue(setUrlStub.lastCall.calledWithExactly('/dashboard/self')); |
| }); |
| |
| test('A fires an error event when not logged in', async () => { |
| element.userModel.setAccount(undefined); |
| const loggedInErrorSpy = sinon.spy(); |
| element.addEventListener('show-auth-required', loggedInErrorSpy); |
| pressKey(element, 'a'); |
| await element.updateComplete; |
| assertIsDefined(element.replyOverlay); |
| assert.isFalse(element.replyOverlay.opened); |
| assert.isTrue(loggedInErrorSpy.called); |
| }); |
| |
| test('shift A does not open reply overlay', async () => { |
| pressKey(element, 'a', Modifier.SHIFT_KEY); |
| await element.updateComplete; |
| assertIsDefined(element.replyOverlay); |
| assert.isFalse(element.replyOverlay.opened); |
| }); |
| |
| test('A toggles overlay when logged in', async () => { |
| element.change = { |
| ...createChangeViewChange(), |
| revisions: createRevisions(1), |
| messages: createChangeMessages(1), |
| }; |
| element.change.labels = {}; |
| await element.updateComplete; |
| |
| const openSpy = sinon.spy(element, 'openReplyDialog'); |
| |
| pressKey(element, 'a'); |
| await element.updateComplete; |
| assertIsDefined(element.replyOverlay); |
| assert.isTrue(element.replyOverlay.opened); |
| element.replyOverlay.close(); |
| assert.isFalse(element.replyOverlay.opened); |
| assert( |
| openSpy.lastCall.calledWithExactly(FocusTarget.ANY), |
| 'openReplyDialog should have been passed ANY' |
| ); |
| assert.equal(openSpy.callCount, 1); |
| }); |
| |
| test('fullscreen-overlay-opened hides content', async () => { |
| element.loggedIn = true; |
| element.loading = false; |
| element.change = { |
| ...createChangeViewChange(), |
| labels: {}, |
| actions: { |
| abandon: { |
| enabled: true, |
| label: 'Abandon', |
| method: HttpMethod.POST, |
| title: 'Abandon', |
| }, |
| }, |
| }; |
| await element.updateComplete; |
| const handlerSpy = sinon.spy(element, 'handleHideBackgroundContent'); |
| const overlay = queryAndAssert<GrOverlay>(element, '#replyOverlay'); |
| overlay.dispatchEvent( |
| new CustomEvent('fullscreen-overlay-opened', { |
| composed: true, |
| bubbles: true, |
| }) |
| ); |
| await element.updateComplete; |
| assert.isTrue(handlerSpy.called); |
| assertIsDefined(element.mainContent); |
| assertIsDefined(element.actions); |
| assert.isTrue(element.mainContent.classList.contains('overlayOpen')); |
| assert.equal(getComputedStyle(element.actions).display, 'flex'); |
| }); |
| |
| test('fullscreen-overlay-closed shows content', async () => { |
| element.loggedIn = true; |
| element.loading = false; |
| element.change = { |
| ...createChangeViewChange(), |
| labels: {}, |
| actions: { |
| abandon: { |
| enabled: true, |
| label: 'Abandon', |
| method: HttpMethod.POST, |
| title: 'Abandon', |
| }, |
| }, |
| }; |
| await element.updateComplete; |
| const handlerSpy = sinon.spy(element, 'handleShowBackgroundContent'); |
| const overlay = queryAndAssert<GrOverlay>(element, '#replyOverlay'); |
| overlay.dispatchEvent( |
| new CustomEvent('fullscreen-overlay-closed', { |
| composed: true, |
| bubbles: true, |
| }) |
| ); |
| await element.updateComplete; |
| assert.isTrue(handlerSpy.called); |
| assertIsDefined(element.mainContent); |
| assert.isFalse(element.mainContent.classList.contains('overlayOpen')); |
| }); |
| |
| test('expand all messages when expand-diffs fired', () => { |
| assertIsDefined(element.fileList); |
| assertIsDefined(element.fileListHeader); |
| const handleExpand = sinon.stub(element.fileList, 'expandAllDiffs'); |
| element.fileListHeader.dispatchEvent( |
| new CustomEvent('expand-diffs', { |
| composed: true, |
| bubbles: true, |
| }) |
| ); |
| assert.isTrue(handleExpand.called); |
| }); |
| |
| test('collapse all messages when collapse-diffs fired', () => { |
| assertIsDefined(element.fileList); |
| assertIsDefined(element.fileListHeader); |
| const handleCollapse = sinon.stub(element.fileList, 'collapseAllDiffs'); |
| element.fileListHeader.dispatchEvent( |
| new CustomEvent('collapse-diffs', { |
| composed: true, |
| bubbles: true, |
| }) |
| ); |
| assert.isTrue(handleCollapse.called); |
| }); |
| |
| test('X should expand all messages', async () => { |
| await element.updateComplete; |
| const handleExpand = sinon.stub( |
| element.messagesList!, |
| 'handleExpandCollapse' |
| ); |
| pressKey(element, 'x'); |
| assert(handleExpand.calledWith(true)); |
| }); |
| |
| test('Z should collapse all messages', async () => { |
| await element.updateComplete; |
| const handleExpand = sinon.stub( |
| element.messagesList!, |
| 'handleExpandCollapse' |
| ); |
| pressKey(element, 'z'); |
| assert(handleExpand.calledWith(false)); |
| }); |
| |
| test('d should open download overlay', () => { |
| assertIsDefined(element.downloadOverlay); |
| const stub = sinon |
| .stub(element.downloadOverlay, 'open') |
| .returns(Promise.resolve()); |
| pressKey(element, 'd'); |
| assert.isTrue(stub.called); |
| }); |
| |
| test(', should open diff preferences', async () => { |
| assertIsDefined(element.fileList); |
| await element.fileList.updateComplete; |
| assertIsDefined(element.fileList.diffPreferencesDialog); |
| const stub = sinon.stub(element.fileList.diffPreferencesDialog, 'open'); |
| element.loggedIn = false; |
| pressKey(element, ','); |
| assert.isFalse(stub.called); |
| |
| element.loggedIn = true; |
| pressKey(element, ','); |
| assert.isTrue(stub.called); |
| }); |
| |
| test('m should toggle diff mode', async () => { |
| const updatePreferencesStub = stubUsers('updatePreferences'); |
| await element.updateComplete; |
| |
| const prefs = { |
| ...createDefaultPreferences(), |
| diff_view: DiffViewMode.SIDE_BY_SIDE, |
| }; |
| element.userModel.setPreferences(prefs); |
| element.handleToggleDiffMode(); |
| assert.isTrue( |
| updatePreferencesStub.calledWith({diff_view: DiffViewMode.UNIFIED}) |
| ); |
| |
| const newPrefs = { |
| ...createDefaultPreferences(), |
| diff_view: DiffViewMode.UNIFIED, |
| }; |
| element.userModel.setPreferences(newPrefs); |
| await element.updateComplete; |
| element.handleToggleDiffMode(); |
| assert.isTrue( |
| updatePreferencesStub.calledWith({diff_view: DiffViewMode.SIDE_BY_SIDE}) |
| ); |
| }); |
| }); |
| |
| suite('thread list and change log tabs', () => { |
| setup(() => { |
| element.changeNum = TEST_NUMERIC_CHANGE_ID; |
| element.patchRange = { |
| basePatchNum: PARENT, |
| patchNum: 1 as RevisionPatchSetNum, |
| }; |
| element.change = { |
| ...createChangeViewChange(), |
| revisions: { |
| rev2: createRevision(2), |
| rev1: createRevision(1), |
| rev13: createRevision(13), |
| rev3: createRevision(3), |
| }, |
| current_revision: 'rev3' as CommitId, |
| status: ChangeStatus.NEW, |
| labels: { |
| test: { |
| all: [], |
| default_value: 0, |
| values: {}, |
| approved: {}, |
| }, |
| }, |
| }; |
| const relatedChanges = element.shadowRoot!.querySelector( |
| '#relatedChanges' |
| ) as GrRelatedChangesList; |
| sinon.stub(relatedChanges, 'reload'); |
| sinon.stub(element, 'loadData').returns(Promise.resolve()); |
| sinon.spy(element, 'viewStateChanged'); |
| element.viewState = createChangeViewState(); |
| }); |
| }); |
| |
| suite('Comments tab', () => { |
| setup(async () => { |
| element.changeNum = TEST_NUMERIC_CHANGE_ID; |
| element.change = { |
| ...createChangeViewChange(), |
| revisions: { |
| rev2: createRevision(2), |
| rev1: createRevision(1), |
| rev13: createRevision(13), |
| rev3: createRevision(3), |
| rev4: createRevision(4), |
| }, |
| current_revision: 'rev4' as CommitId, |
| }; |
| element.commentThreads = THREADS; |
| await element.updateComplete; |
| const paperTabs = element.shadowRoot!.querySelector('#tabs')!; |
| const tabs = paperTabs.querySelectorAll('paper-tab'); |
| assert.isTrue(tabs.length > 1); |
| assert.equal(tabs[1].dataset.name, 'comments'); |
| tabs[1].click(); |
| await element.updateComplete; |
| }); |
| |
| test('commentId overrides unresolveOnly default', async () => { |
| const threadList = queryAndAssert<GrThreadList>( |
| element, |
| 'gr-thread-list' |
| ); |
| assert.isTrue(element.unresolvedOnly); |
| assert.isNotOk(element.scrollCommentId); |
| assert.isTrue(threadList.unresolvedOnly); |
| |
| element.scrollCommentId = 'abcd' as UrlEncodedCommentId; |
| await element.updateComplete; |
| assert.isFalse(threadList.unresolvedOnly); |
| }); |
| }); |
| |
| suite('Findings robot-comment tab', () => { |
| setup(async () => { |
| element.changeNum = TEST_NUMERIC_CHANGE_ID; |
| element.change = { |
| ...createChangeViewChange(), |
| revisions: { |
| rev2: createRevision(2), |
| rev1: createRevision(1), |
| rev13: createRevision(13), |
| rev3: createRevision(3), |
| rev4: createRevision(4), |
| }, |
| current_revision: 'rev4' as CommitId, |
| }; |
| element.commentThreads = THREADS; |
| element.showFindingsTab = true; |
| await element.updateComplete; |
| const paperTabs = element.shadowRoot!.querySelector('#tabs')!; |
| const tabs = paperTabs.querySelectorAll('paper-tab'); |
| assert.isTrue(tabs.length > 3); |
| assert.equal(tabs[3].dataset.name, 'findings'); |
| tabs[3].click(); |
| await element.updateComplete; |
| }); |
| |
| test('robot comments count per patchset', () => { |
| const count = element.robotCommentCountPerPatchSet(THREADS); |
| const expectedCount = { |
| 2: 1, |
| 3: 1, |
| 4: 2, |
| }; |
| assert.deepEqual(count, expectedCount); |
| assert.equal( |
| element.computeText(createRevision(2), THREADS), |
| 'Patchset 2 (1 finding)' |
| ); |
| assert.equal( |
| element.computeText(createRevision(4), THREADS), |
| 'Patchset 4 (2 findings)' |
| ); |
| assert.equal( |
| element.computeText(createRevision(5), THREADS), |
| 'Patchset 5' |
| ); |
| }); |
| |
| test('only robot comments are rendered', () => { |
| assert.equal(element.computeRobotCommentThreads().length, 2); |
| assert.equal( |
| ( |
| element.computeRobotCommentThreads()[0] |
| .comments[0] as RobotCommentInfo |
| ).robot_id, |
| 'rc1' |
| ); |
| assert.equal( |
| ( |
| element.computeRobotCommentThreads()[1] |
| .comments[0] as RobotCommentInfo |
| ).robot_id, |
| 'rc2' |
| ); |
| }); |
| |
| test('changing patchsets resets robot comments', async () => { |
| assertIsDefined(element.change); |
| const newChange = {...element.change}; |
| newChange.current_revision = 'rev3' as CommitId; |
| element.change = newChange; |
| await element.updateComplete; |
| assert.equal(element.computeRobotCommentThreads().length, 1); |
| }); |
| |
| test('Show more button is hidden', () => { |
| assert.isNull(element.shadowRoot!.querySelector('.show-robot-comments')); |
| }); |
| |
| suite('robot comments show more button', () => { |
| setup(async () => { |
| const arr = []; |
| for (let i = 0; i <= 30; i++) { |
| arr.push(...THREADS); |
| } |
| element.commentThreads = arr; |
| await element.updateComplete; |
| }); |
| |
| test('Show more button is rendered', () => { |
| assert.isOk(element.shadowRoot!.querySelector('.show-robot-comments')); |
| assert.equal( |
| element.computeRobotCommentThreads().length, |
| ROBOT_COMMENTS_LIMIT |
| ); |
| }); |
| |
| test('Clicking show more button renders all comments', async () => { |
| element |
| .shadowRoot!.querySelector<GrButton>('.show-robot-comments')! |
| .click(); |
| await element.updateComplete; |
| assert.equal(element.computeRobotCommentThreads().length, 62); |
| }); |
| }); |
| }); |
| |
| test('reply button is not visible when logged out', async () => { |
| assertIsDefined(element.replyBtn); |
| element.loggedIn = false; |
| await element.updateComplete; |
| assert.equal(getComputedStyle(element.replyBtn).display, 'none'); |
| element.loggedIn = true; |
| await element.updateComplete; |
| assert.notEqual(getComputedStyle(element.replyBtn).display, 'none'); |
| }); |
| |
| test('download tap calls handleOpenDownloadDialog', () => { |
| assertIsDefined(element.actions); |
| const openDialogStub = sinon.stub(element, 'handleOpenDownloadDialog'); |
| element.actions.dispatchEvent( |
| new CustomEvent('download-tap', { |
| composed: true, |
| bubbles: true, |
| }) |
| ); |
| assert.isTrue(openDialogStub.called); |
| }); |
| |
| test('fetches the server config on attached', async () => { |
| await element.updateComplete; |
| assert.equal( |
| element.serverConfig!.user.anonymous_coward_name, |
| 'test coward name' |
| ); |
| }); |
| |
| test('changeStatuses', async () => { |
| element.loading = false; |
| element.change = { |
| ...createChangeViewChange(), |
| revisions: { |
| rev2: createRevision(2), |
| rev1: createRevision(1), |
| rev13: createRevision(13), |
| rev3: createRevision(3), |
| }, |
| current_revision: 'rev3' as CommitId, |
| status: ChangeStatus.MERGED, |
| labels: { |
| test: { |
| all: [], |
| default_value: 0, |
| values: {}, |
| approved: {}, |
| }, |
| }, |
| }; |
| element.mergeable = true; |
| await element.updateComplete; |
| const expectedStatuses = [ChangeStates.MERGED]; |
| assert.deepEqual(element.changeStatuses, expectedStatuses); |
| const statusChips = |
| element.shadowRoot!.querySelectorAll('gr-change-status'); |
| assert.equal(statusChips.length, 1); |
| }); |
| |
| suite('ChangeStatus revert', () => { |
| test('do not show any chip if no revert created', async () => { |
| const change = { |
| ...createParsedChange(), |
| messages: createChangeMessages(2), |
| }; |
| const getChangeStub = stubRestApi('getChange'); |
| getChangeStub.onFirstCall().returns( |
| Promise.resolve({ |
| ...createChange(), |
| }) |
| ); |
| getChangeStub.onSecondCall().returns( |
| Promise.resolve({ |
| ...createChange(), |
| }) |
| ); |
| element.change = change; |
| element.mergeable = true; |
| element.currentRevisionActions = {submit: {enabled: true}}; |
| assert.isTrue(element.isSubmitEnabled()); |
| await element.updateComplete; |
| element.computeRevertSubmitted(element.change); |
| await element.updateComplete; |
| assert.isFalse( |
| element.changeStatuses?.includes(ChangeStates.REVERT_SUBMITTED) |
| ); |
| assert.isFalse( |
| element.changeStatuses?.includes(ChangeStates.REVERT_CREATED) |
| ); |
| }); |
| |
| test('do not show any chip if all reverts are abandoned', async () => { |
| const change = { |
| ...createParsedChange(), |
| messages: createChangeMessages(2), |
| }; |
| change.messages[0].message = 'Created a revert of this change as 12345'; |
| change.messages[0].tag = MessageTag.TAG_REVERT as ReviewInputTag; |
| |
| change.messages[1].message = 'Created a revert of this change as 23456'; |
| change.messages[1].tag = MessageTag.TAG_REVERT as ReviewInputTag; |
| |
| const getChangeStub = stubRestApi('getChange'); |
| getChangeStub.onFirstCall().returns( |
| Promise.resolve({ |
| ...createChange(), |
| status: ChangeStatus.ABANDONED, |
| }) |
| ); |
| getChangeStub.onSecondCall().returns( |
| Promise.resolve({ |
| ...createChange(), |
| status: ChangeStatus.ABANDONED, |
| }) |
| ); |
| element.change = change; |
| element.mergeable = true; |
| element.currentRevisionActions = {submit: {enabled: true}}; |
| assert.isTrue(element.isSubmitEnabled()); |
| await element.updateComplete; |
| element.computeRevertSubmitted(element.change); |
| await element.updateComplete; |
| assert.isFalse( |
| element.changeStatuses?.includes(ChangeStates.REVERT_SUBMITTED) |
| ); |
| assert.isFalse( |
| element.changeStatuses?.includes(ChangeStates.REVERT_CREATED) |
| ); |
| }); |
| |
| test('show revert created if no revert is merged', async () => { |
| const change = { |
| ...createParsedChange(), |
| messages: createChangeMessages(2), |
| }; |
| change.messages[0].message = 'Created a revert of this change as 12345'; |
| change.messages[0].tag = MessageTag.TAG_REVERT as ReviewInputTag; |
| |
| change.messages[1].message = 'Created a revert of this change as 23456'; |
| change.messages[1].tag = MessageTag.TAG_REVERT as ReviewInputTag; |
| |
| const getChangeStub = stubRestApi('getChange'); |
| getChangeStub.onFirstCall().returns( |
| Promise.resolve({ |
| ...createChange(), |
| }) |
| ); |
| getChangeStub.onSecondCall().returns( |
| Promise.resolve({ |
| ...createChange(), |
| }) |
| ); |
| element.change = change; |
| element.mergeable = true; |
| element.currentRevisionActions = {submit: {enabled: true}}; |
| assert.isTrue(element.isSubmitEnabled()); |
| await element.updateComplete; |
| element.computeRevertSubmitted(element.change); |
| // Wait for promises to settle. |
| await waitEventLoop(); |
| await element.updateComplete; |
| assert.isFalse( |
| element.changeStatuses?.includes(ChangeStates.REVERT_SUBMITTED) |
| ); |
| assert.isTrue( |
| element.changeStatuses?.includes(ChangeStates.REVERT_CREATED) |
| ); |
| }); |
| |
| test('show revert submitted if revert is merged', async () => { |
| const change = { |
| ...createParsedChange(), |
| messages: createChangeMessages(2), |
| }; |
| change.messages[0].message = 'Created a revert of this change as 12345'; |
| change.messages[0].tag = MessageTag.TAG_REVERT as ReviewInputTag; |
| const getChangeStub = stubRestApi('getChange'); |
| getChangeStub.onFirstCall().returns( |
| Promise.resolve({ |
| ...createChange(), |
| status: ChangeStatus.MERGED, |
| }) |
| ); |
| getChangeStub.onSecondCall().returns( |
| Promise.resolve({ |
| ...createChange(), |
| }) |
| ); |
| element.change = change; |
| element.mergeable = true; |
| element.currentRevisionActions = {submit: {enabled: true}}; |
| assert.isTrue(element.isSubmitEnabled()); |
| await element.updateComplete; |
| element.computeRevertSubmitted(element.change); |
| // Wait for promises to settle. |
| await waitEventLoop(); |
| await element.updateComplete; |
| assert.isFalse( |
| element.changeStatuses?.includes(ChangeStates.REVERT_CREATED) |
| ); |
| assert.isTrue( |
| element.changeStatuses?.includes(ChangeStates.REVERT_SUBMITTED) |
| ); |
| }); |
| }); |
| |
| test('diff preferences open when open-diff-prefs is fired', async () => { |
| await element.updateComplete; |
| assertIsDefined(element.fileList); |
| assertIsDefined(element.fileListHeader); |
| await element.fileList.updateComplete; |
| const overlayOpenStub = sinon.stub(element.fileList, 'openDiffPrefs'); |
| element.fileListHeader.dispatchEvent( |
| new CustomEvent('open-diff-prefs', { |
| composed: true, |
| bubbles: true, |
| }) |
| ); |
| assert.isTrue(overlayOpenStub.called); |
| }); |
| |
| test('prepareCommitMsgForLinkify', () => { |
| let commitMessage = 'R=test@google.com'; |
| let result = element.prepareCommitMsgForLinkify(commitMessage); |
| assert.equal(result, 'R=\u200Btest@google.com'); |
| |
| commitMessage = 'R=test@google.com\nR=test@google.com'; |
| result = element.prepareCommitMsgForLinkify(commitMessage); |
| assert.equal(result, 'R=\u200Btest@google.com\nR=\u200Btest@google.com'); |
| |
| commitMessage = 'CC=test@google.com'; |
| result = element.prepareCommitMsgForLinkify(commitMessage); |
| assert.equal(result, 'CC=\u200Btest@google.com'); |
| }); |
| |
| test('_isSubmitEnabled', () => { |
| assert.isFalse(element.isSubmitEnabled()); |
| element.currentRevisionActions = {submit: {}}; |
| assert.isFalse(element.isSubmitEnabled()); |
| element.currentRevisionActions = {submit: {enabled: true}}; |
| assert.isTrue(element.isSubmitEnabled()); |
| }); |
| |
| test('reload is called when an approved label is removed', async () => { |
| const vote: ApprovalInfo = { |
| ...createApproval(), |
| _account_id: 1 as AccountId, |
| name: 'bojack', |
| value: 1, |
| }; |
| element.changeNum = TEST_NUMERIC_CHANGE_ID; |
| element.patchRange = { |
| basePatchNum: PARENT, |
| patchNum: 1 as RevisionPatchSetNum, |
| }; |
| const change = { |
| ...createParsedChange(), |
| owner: createAccountWithIdNameAndEmail(), |
| revisions: { |
| rev2: createRevision(2), |
| rev1: createRevision(1), |
| rev13: createRevision(13), |
| rev3: createRevision(3), |
| }, |
| current_revision: 'rev3' as CommitId, |
| status: ChangeStatus.NEW, |
| labels: { |
| test: { |
| all: [vote], |
| default_value: 0, |
| values: {}, |
| approved: {}, |
| }, |
| }, |
| }; |
| element.change = change; |
| await element.updateComplete; |
| const reloadStub = sinon.stub(element, 'loadData'); |
| const newChange = {...element.change}; |
| (newChange.labels!.test! as DetailedLabelInfo).all = []; |
| element.change = deepClone(newChange); |
| await element.updateComplete; |
| assert.isFalse(reloadStub.called); |
| |
| assert.isDefined(element.change); |
| const testLabels: DetailedLabelInfo & QuickLabelInfo = |
| newChange.labels!.test; |
| assertIsDefined(testLabels); |
| testLabels.all!.push(vote); |
| testLabels.all!.push(vote); |
| testLabels.approved = vote; |
| element.change = deepClone(newChange); |
| await element.updateComplete; |
| assert.isFalse(reloadStub.called); |
| |
| assert.isDefined(element.change); |
| (newChange.labels!.test! as DetailedLabelInfo).all = []; |
| element.change = deepClone(newChange); |
| await element.updateComplete; |
| assert.isTrue(reloadStub.called); |
| assert.isTrue(reloadStub.calledOnce); |
| }); |
| |
| test('reply button has updated count when there are drafts', () => { |
| const getLabel = (canReview: boolean) => { |
| element.change!.actions!.ready = {enabled: canReview}; |
| return element.computeReplyButtonLabel(); |
| }; |
| element.change = createParsedChange(); |
| element.change.actions = {}; |
| element.diffDrafts = undefined; |
| assert.equal(getLabel(false), 'Reply'); |
| assert.equal(getLabel(true), 'Reply'); |
| |
| element.diffDrafts = {}; |
| assert.equal(getLabel(false), 'Reply'); |
| assert.equal(getLabel(true), 'Start Review'); |
| |
| element.diffDrafts = { |
| 'file1.txt': [createDraft()], |
| 'file2.txt': [createDraft(), createDraft()], |
| }; |
| assert.equal(getLabel(false), 'Reply (3)'); |
| assert.equal(getLabel(true), 'Start Review (3)'); |
| }); |
| |
| test('change num change', async () => { |
| const change = { |
| ...createChangeViewChange(), |
| labels: {}, |
| } as ParsedChangeInfo; |
| element.changeNum = undefined; |
| element.patchRange = { |
| basePatchNum: PARENT, |
| patchNum: 2 as RevisionPatchSetNum, |
| }; |
| element.change = change; |
| assertIsDefined(element.fileList); |
| assert.equal(element.fileList.numFilesShown, DEFAULT_NUM_FILES_SHOWN); |
| element.fileList.numFilesShown = 150; |
| element.fileList.selectedIndex = 15; |
| await element.updateComplete; |
| |
| element.changeNum = 2 as NumericChangeId; |
| element.viewState = { |
| ...createChangeViewState(), |
| changeNum: 2 as NumericChangeId, |
| }; |
| await element.updateComplete; |
| assert.equal(element.fileList.numFilesShown, DEFAULT_NUM_FILES_SHOWN); |
| assert.equal(element.fileList.selectedIndex, 0); |
| }); |
| |
| test('don’t reload entire page when patchRange changes', async () => { |
| const reloadStub = sinon |
| .stub(element, 'loadData') |
| .callsFake(() => Promise.resolve()); |
| const reloadPatchDependentStub = sinon |
| .stub(element, 'reloadPatchNumDependentResources') |
| .callsFake(() => Promise.resolve([undefined, undefined, undefined])); |
| assertIsDefined(element.fileList); |
| await element.fileList.updateComplete; |
| const collapseStub = sinon.stub(element.fileList, 'collapseAllDiffs'); |
| const value: ChangeViewState = { |
| ...createChangeViewState(), |
| view: GerritView.CHANGE, |
| patchNum: 1 as RevisionPatchSetNum, |
| }; |
| element.changeNum = undefined; |
| element.viewState = value; |
| await element.updateComplete; |
| assert.isTrue(reloadStub.calledOnce); |
| |
| element.initialLoadComplete = true; |
| element.fileList.selectedIndex = 15; |
| element.change = { |
| ...createChangeViewChange(), |
| revisions: { |
| rev1: createRevision(1), |
| rev2: createRevision(2), |
| }, |
| }; |
| |
| value.basePatchNum = 1 as BasePatchSetNum; |
| value.patchNum = 2 as RevisionPatchSetNum; |
| element.viewState = {...value}; |
| await element.updateComplete; |
| await waitEventLoop(); |
| assert.equal(element.fileList.selectedIndex, 0); |
| assert.isFalse(reloadStub.calledTwice); |
| assert.isTrue(reloadPatchDependentStub.calledOnce); |
| assert.isTrue(collapseStub.calledTwice); |
| }); |
| |
| test('reload ported comments when patchNum changes', async () => { |
| assertIsDefined(element.fileList); |
| sinon.stub(element, 'loadData').callsFake(() => Promise.resolve()); |
| sinon.stub(element, 'loadAndSetCommitInfo'); |
| await element.updateComplete; |
| const reloadPortedCommentsStub = sinon.stub( |
| element.getCommentsModel(), |
| 'reloadPortedComments' |
| ); |
| const reloadPortedDraftsStub = sinon.stub( |
| element.getCommentsModel(), |
| 'reloadPortedDrafts' |
| ); |
| sinon.stub(element.fileList, 'collapseAllDiffs'); |
| |
| const value: ChangeViewState = { |
| ...createChangeViewState(), |
| view: GerritView.CHANGE, |
| patchNum: 1 as RevisionPatchSetNum, |
| }; |
| element.viewState = value; |
| await element.updateComplete; |
| |
| element.initialLoadComplete = true; |
| element.change = { |
| ...createChangeViewChange(), |
| revisions: { |
| rev1: createRevision(1), |
| rev2: createRevision(2), |
| }, |
| }; |
| |
| value.basePatchNum = 1 as BasePatchSetNum; |
| value.patchNum = 2 as RevisionPatchSetNum; |
| element.viewState = {...value}; |
| await element.updateComplete; |
| assert.isTrue(reloadPortedCommentsStub.calledOnce); |
| assert.isTrue(reloadPortedDraftsStub.calledOnce); |
| }); |
| |
| test('do not reload entire page when patchRange doesnt change', async () => { |
| assertIsDefined(element.fileList); |
| const reloadStub = sinon |
| .stub(element, 'loadData') |
| .callsFake(() => Promise.resolve()); |
| const collapseStub = sinon.stub(element.fileList, 'collapseAllDiffs'); |
| const value: ChangeViewState = createChangeViewState(); |
| element.viewState = value; |
| // change already loaded |
| assert.isOk(element.changeNum); |
| await element.updateComplete; |
| assert.isFalse(reloadStub.calledOnce); |
| element.initialLoadComplete = true; |
| element.viewState = {...value}; |
| await element.updateComplete; |
| assert.isFalse(reloadStub.calledTwice); |
| assert.isFalse(collapseStub.calledTwice); |
| }); |
| |
| test('forceReload updates the change', async () => { |
| assertIsDefined(element.fileList); |
| const getChangeStub = stubRestApi('getChangeDetail').returns( |
| Promise.resolve(createParsedChange()) |
| ); |
| const loadDataStub = sinon |
| .stub(element, 'loadData') |
| .callsFake(() => Promise.resolve()); |
| const collapseStub = sinon.stub(element.fileList, 'collapseAllDiffs'); |
| element.viewState = {...createChangeViewState(), forceReload: true}; |
| await element.updateComplete; |
| assert.isTrue(getChangeStub.called); |
| assert.isTrue(loadDataStub.called); |
| assert.isTrue(collapseStub.called); |
| // patchNum is set by changeChanged, so this verifies that change was set. |
| assert.isOk(element.patchRange?.patchNum); |
| }); |
| |
| test('do not handle new change numbers', async () => { |
| const recreateSpy = sinon.spy(); |
| element.addEventListener('recreate-change-view', recreateSpy); |
| |
| const value: ChangeViewState = createChangeViewState(); |
| element.viewState = value; |
| await element.updateComplete; |
| assert.isFalse(recreateSpy.calledOnce); |
| |
| value.changeNum = 555111333 as NumericChangeId; |
| element.viewState = {...value}; |
| await element.updateComplete; |
| assert.isTrue(recreateSpy.calledOnce); |
| }); |
| |
| test('related changes are updated when loadData is called', async () => { |
| await element.updateComplete; |
| const relatedChanges = element.shadowRoot!.querySelector( |
| '#relatedChanges' |
| ) as GrRelatedChangesList; |
| const reloadStub = sinon.stub(relatedChanges, 'reload'); |
| stubRestApi('getMergeable').returns( |
| Promise.resolve({...createMergeable(), mergeable: true}) |
| ); |
| |
| element.viewState = createChangeViewState(); |
| element.getChangeModel().setState({ |
| loadingStatus: LoadingStatus.LOADED, |
| change: { |
| ...createChangeViewChange(), |
| }, |
| }); |
| |
| await element.loadData(true); |
| assert.isFalse(setUrlStub.called); |
| assert.isTrue(reloadStub.called); |
| }); |
| |
| test('computeCopyTextForTitle', () => { |
| element.change = { |
| ...createChangeViewChange(), |
| _number: 123 as NumericChangeId, |
| subject: 'test subject', |
| revisions: { |
| rev1: createRevision(1), |
| rev3: createRevision(3), |
| }, |
| current_revision: 'rev3' as CommitId, |
| }; |
| assert.equal( |
| element.computeCopyTextForTitle(), |
| `123: test subject | http://${location.host}/c/test-project/+/123` |
| ); |
| }); |
| |
| test('get latest revision', () => { |
| let change: ChangeInfo = { |
| ...createChange(), |
| revisions: { |
| rev1: createRevision(1), |
| rev3: createRevision(3), |
| }, |
| current_revision: 'rev3' as CommitId, |
| }; |
| assert.equal(element.getLatestRevisionSHA(change), 'rev3'); |
| change = { |
| ...createChange(), |
| revisions: { |
| rev1: createRevision(1), |
| }, |
| current_revision: undefined, |
| }; |
| assert.equal(element.getLatestRevisionSHA(change), 'rev1'); |
| }); |
| |
| test('show commit message edit button', () => { |
| const change = createParsedChange(); |
| const mergedChanged: ParsedChangeInfo = { |
| ...createParsedChange(), |
| status: ChangeStatus.MERGED, |
| }; |
| assert.isTrue(element.computeHideEditCommitMessage(false, false, change)); |
| assert.isTrue(element.computeHideEditCommitMessage(true, true, change)); |
| assert.isTrue(element.computeHideEditCommitMessage(false, true, change)); |
| assert.isFalse(element.computeHideEditCommitMessage(true, false, change)); |
| assert.isTrue( |
| element.computeHideEditCommitMessage(true, false, mergedChanged) |
| ); |
| assert.isTrue( |
| element.computeHideEditCommitMessage(true, false, change, true) |
| ); |
| assert.isFalse( |
| element.computeHideEditCommitMessage(true, false, change, false) |
| ); |
| }); |
| |
| test('handleCommitMessageSave trims trailing whitespace', async () => { |
| element.change = createChangeViewChange(); |
| // Response code is 500, because we want to avoid window reloading |
| const putStub = stubRestApi('putChangeCommitMessage').returns( |
| Promise.resolve(new Response(null, {status: 500})) |
| ); |
| await element.updateComplete; |
| const mockEvent = (content: string) => |
| new CustomEvent('', {detail: {content}}); |
| |
| assertIsDefined(element.commitMessageEditor); |
| element.handleCommitMessageSave(mockEvent('test \n test ')); |
| assert.equal(putStub.lastCall.args[1], 'test\n test'); |
| element.commitMessageEditor.disabled = false; |
| element.handleCommitMessageSave(mockEvent(' test\ntest')); |
| assert.equal(putStub.lastCall.args[1], ' test\ntest'); |
| element.commitMessageEditor.disabled = false; |
| element.handleCommitMessageSave(mockEvent('\n\n\n\n\n\n\n\n')); |
| assert.equal(putStub.lastCall.args[1], '\n\n\n\n\n\n\n\n'); |
| }); |
| test('computeChangeIdCommitMessageError', () => { |
| let commitMessage = 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282483'; |
| let change: ParsedChangeInfo = { |
| ...createChangeViewChange(), |
| change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282483' as ChangeId, |
| }; |
| assert.equal( |
| element.computeChangeIdCommitMessageError(commitMessage, change), |
| null |
| ); |
| |
| change = { |
| ...createChangeViewChange(), |
| change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282484' as ChangeId, |
| }; |
| assert.equal( |
| element.computeChangeIdCommitMessageError(commitMessage, change), |
| 'mismatch' |
| ); |
| |
| commitMessage = 'This is the greatest change.'; |
| assert.equal( |
| element.computeChangeIdCommitMessageError(commitMessage, change), |
| 'missing' |
| ); |
| }); |
| |
| test('multiple change Ids in commit message picks last', () => { |
| const commitMessage = [ |
| 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282484', |
| 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282483', |
| ].join('\n'); |
| let change: ParsedChangeInfo = { |
| ...createChangeViewChange(), |
| change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282483' as ChangeId, |
| }; |
| assert.equal( |
| element.computeChangeIdCommitMessageError(commitMessage, change), |
| null |
| ); |
| change = { |
| ...createChangeViewChange(), |
| change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282484' as ChangeId, |
| }; |
| assert.equal( |
| element.computeChangeIdCommitMessageError(commitMessage, change), |
| 'mismatch' |
| ); |
| }); |
| |
| test('does not count change Id that starts mid line', () => { |
| const commitMessage = [ |
| 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282484', |
| 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282483', |
| ].join(' and '); |
| let change: ParsedChangeInfo = { |
| ...createChangeViewChange(), |
| change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282484' as ChangeId, |
| }; |
| assert.equal( |
| element.computeChangeIdCommitMessageError(commitMessage, change), |
| null |
| ); |
| change = { |
| ...createChangeViewChange(), |
| change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282483' as ChangeId, |
| }; |
| assert.equal( |
| element.computeChangeIdCommitMessageError(commitMessage, change), |
| 'mismatch' |
| ); |
| }); |
| |
| test('computeTitleAttributeWarning', () => { |
| let changeIdCommitMessageError = 'missing'; |
| assert.equal( |
| element.computeTitleAttributeWarning(changeIdCommitMessageError), |
| 'No Change-Id in commit message' |
| ); |
| |
| changeIdCommitMessageError = 'mismatch'; |
| assert.equal( |
| element.computeTitleAttributeWarning(changeIdCommitMessageError), |
| 'Change-Id mismatch' |
| ); |
| }); |
| |
| test('computeChangeIdClass', () => { |
| let changeIdCommitMessageError = 'missing'; |
| assert.equal(element.computeChangeIdClass(changeIdCommitMessageError), ''); |
| |
| changeIdCommitMessageError = 'mismatch'; |
| assert.equal( |
| element.computeChangeIdClass(changeIdCommitMessageError), |
| 'warning' |
| ); |
| }); |
| |
| test('topic is coalesced to null', async () => { |
| sinon.stub(element, 'changeChanged'); |
| element.getChangeModel().setState({ |
| loadingStatus: LoadingStatus.LOADED, |
| change: { |
| ...createChangeViewChange(), |
| labels: {}, |
| current_revision: 'foo' as CommitId, |
| revisions: {foo: createRevision()}, |
| }, |
| }); |
| |
| await element.performPostChangeLoadTasks(); |
| assert.isNull(element.change!.topic); |
| }); |
| |
| test('commit sha is populated from getChangeDetail', async () => { |
| element.getChangeModel().setState({ |
| loadingStatus: LoadingStatus.LOADED, |
| change: { |
| ...createChangeViewChange(), |
| labels: {}, |
| current_revision: 'foo' as CommitId, |
| revisions: {foo: createRevision()}, |
| }, |
| }); |
| |
| await element.performPostChangeLoadTasks(); |
| assert.equal('foo', element.commitInfo!.commit); |
| }); |
| |
| test('getBasePatchNum', async () => { |
| element.change = { |
| ...createChangeViewChange(), |
| revisions: { |
| '98da160735fb81604b4c40e93c368f380539dd0e': createRevision(), |
| }, |
| }; |
| element.patchRange = { |
| basePatchNum: PARENT, |
| }; |
| await element.updateComplete; |
| assert.equal(element.getBasePatchNum(), PARENT); |
| |
| element.prefs = { |
| ...createPreferences(), |
| default_base_for_merges: DefaultBase.FIRST_PARENT, |
| }; |
| |
| element.change = { |
| ...createChangeViewChange(), |
| revisions: { |
| '98da160735fb81604b4c40e93c368f380539dd0e': { |
| ...createRevision(1), |
| commit: { |
| ...createCommit(), |
| parents: [ |
| { |
| commit: '6e12bdf1176eb4ab24d8491ba3b6d0704409cde8' as CommitId, |
| subject: 'test', |
| }, |
| { |
| commit: '22f7db4754b5d9816fc581f3d9a6c0ef8429c841' as CommitId, |
| subject: 'test3', |
| }, |
| ], |
| }, |
| }, |
| }, |
| }; |
| await element.updateComplete; |
| assert.equal(element.getBasePatchNum(), -1 as BasePatchSetNum); |
| |
| element.patchRange.basePatchNum = PARENT; |
| element.patchRange.patchNum = 1 as RevisionPatchSetNum; |
| await element.updateComplete; |
| assert.equal(element.getBasePatchNum(), PARENT); |
| }); |
| |
| test('openReplyDialog called with `ANY` when coming from tap event', async () => { |
| await element.updateComplete; |
| assertIsDefined(element.replyBtn); |
| const openStub = sinon.stub(element, 'openReplyDialog'); |
| element.replyBtn.click(); |
| assert( |
| openStub.lastCall.calledWithExactly(FocusTarget.ANY), |
| 'openReplyDialog should have been passed ANY' |
| ); |
| assert.equal(openStub.callCount, 1); |
| }); |
| |
| test( |
| 'openReplyDialog called with `BODY` when coming from message reply' + |
| 'event', |
| async () => { |
| await element.updateComplete; |
| const openStub = sinon.stub(element, 'openReplyDialog'); |
| element.messagesList!.dispatchEvent( |
| new CustomEvent('reply', { |
| detail: {message: {message: 'text'}}, |
| composed: true, |
| bubbles: true, |
| }) |
| ); |
| assert.isTrue(openStub.calledOnce); |
| assert.equal(openStub.lastCall.args[0], FocusTarget.BODY); |
| } |
| ); |
| |
| test('reply dialog focus can be controlled', () => { |
| const openStub = sinon.stub(element, 'openReplyDialog'); |
| |
| const e = new CustomEvent('show-reply-dialog', { |
| detail: {value: {ccsOnly: false}}, |
| }); |
| element.handleShowReplyDialog(e); |
| assert( |
| openStub.lastCall.calledWithExactly(FocusTarget.REVIEWERS), |
| 'openReplyDialog should have been passed REVIEWERS' |
| ); |
| assert.equal(openStub.callCount, 1); |
| |
| e.detail.value = {ccsOnly: true}; |
| element.handleShowReplyDialog(e); |
| assert( |
| openStub.lastCall.calledWithExactly(FocusTarget.CCS), |
| 'openReplyDialog should have been passed CCS' |
| ); |
| assert.equal(openStub.callCount, 2); |
| }); |
| |
| test('getUrlParameter functionality', () => { |
| const locationStub = sinon.stub(element, 'getLocationSearch'); |
| locationStub.returns('?test'); |
| assert.equal(element._getUrlParameter('test'), 'test'); |
| locationStub.returns('?test2=12&test=3'); |
| assert.equal(element._getUrlParameter('test'), 'test'); |
| locationStub.returns(''); |
| assert.isNull(element._getUrlParameter('test')); |
| locationStub.returns('?'); |
| assert.isNull(element._getUrlParameter('test')); |
| locationStub.returns('?test2'); |
| assert.isNull(element._getUrlParameter('test')); |
| }); |
| |
| test('revert dialog opened with revert param', async () => { |
| const awaitPluginsLoadedStub = sinon |
| .stub(getPluginLoader(), 'awaitPluginsLoaded') |
| .callsFake(() => Promise.resolve()); |
| |
| element.patchRange = { |
| basePatchNum: PARENT, |
| patchNum: 2 as RevisionPatchSetNum, |
| }; |
| element.change = { |
| ...createChangeViewChange(), |
| revisions: { |
| rev1: createRevision(1), |
| rev2: createRevision(2), |
| }, |
| current_revision: 'rev1' as CommitId, |
| status: ChangeStatus.MERGED, |
| labels: {}, |
| actions: {}, |
| }; |
| |
| sinon.stub(element, '_getUrlParameter').callsFake(param => { |
| assert.equal(param, 'revert'); |
| return param; |
| }); |
| |
| const promise = mockPromise(); |
| assertIsDefined(element.actions); |
| sinon |
| .stub(element.actions, 'showRevertDialog') |
| .callsFake(() => promise.resolve()); |
| |
| element.maybeShowRevertDialog(); |
| assert.isTrue(awaitPluginsLoadedStub.called); |
| await promise; |
| }); |
| |
| suite('reply dialog tests', () => { |
| setup(async () => { |
| element.change = { |
| ...createChangeViewChange(), |
| // element has latest info |
| revisions: {rev1: createRevision()}, |
| messages: createChangeMessages(1), |
| current_revision: 'rev1' as CommitId, |
| labels: {}, |
| }; |
| await element.updateComplete; |
| }); |
| |
| test('show reply dialog on open-reply-dialog event', async () => { |
| const openReplyDialogStub = sinon.stub(element, 'openReplyDialog'); |
| element.dispatchEvent( |
| new CustomEvent('open-reply-dialog', { |
| composed: true, |
| bubbles: true, |
| detail: {}, |
| }) |
| ); |
| await element.updateComplete; |
| assert.isTrue(openReplyDialogStub.calledOnce); |
| }); |
| |
| test('reply from comment adds quote text', async () => { |
| const e = new CustomEvent('', { |
| detail: {message: {message: 'quote text'}}, |
| }); |
| element.handleMessageReply(e); |
| const dialog = await waitQueryAndAssert<GrReplyDialog>( |
| element, |
| '#replyDialog' |
| ); |
| const openSpy = sinon.spy(dialog, 'open'); |
| await element.updateComplete; |
| await waitUntil(() => openSpy.called && !!openSpy.lastCall.args[1]); |
| assert.equal(openSpy.lastCall.args[1], '> quote text\n\n'); |
| }); |
| }); |
| |
| test('header class computation', () => { |
| assert.equal(element.computeHeaderClass(), 'header'); |
| assertIsDefined(element.viewState); |
| element.viewState.edit = true; |
| assert.equal(element.computeHeaderClass(), 'header editMode'); |
| }); |
| |
| test('maybeScrollToMessage', async () => { |
| await element.updateComplete; |
| const scrollStub = sinon.stub(element.messagesList!, 'scrollToMessage'); |
| |
| element.maybeScrollToMessage(''); |
| assert.isFalse(scrollStub.called); |
| element.maybeScrollToMessage('message'); |
| assert.isFalse(scrollStub.called); |
| element.maybeScrollToMessage('#message-TEST'); |
| assert.isTrue(scrollStub.called); |
| assert.equal(scrollStub.lastCall.args[0], 'TEST'); |
| }); |
| |
| test('computeEditMode', async () => { |
| const callCompute = async (viewState: ChangeViewState) => { |
| element.viewState = viewState; |
| await element.updateComplete; |
| return element.getEditMode(); |
| }; |
| assert.isTrue( |
| await callCompute({ |
| ...createChangeViewState(), |
| edit: true, |
| basePatchNum: PARENT, |
| patchNum: 1 as RevisionPatchSetNum, |
| }) |
| ); |
| assert.isFalse( |
| await callCompute({ |
| ...createChangeViewState(), |
| basePatchNum: PARENT, |
| patchNum: 1 as RevisionPatchSetNum, |
| }) |
| ); |
| assert.isTrue( |
| await callCompute({ |
| ...createChangeViewState(), |
| basePatchNum: 1 as BasePatchSetNum, |
| patchNum: EDIT, |
| }) |
| ); |
| }); |
| |
| test('processEdit', () => { |
| element.patchRange = {}; |
| const change: ParsedChangeInfo = { |
| ...createChangeViewChange(), |
| current_revision: 'foo' as CommitId, |
| revisions: { |
| foo: {...createRevision()}, |
| }, |
| }; |
| |
| // With no edit, nothing happens. |
| element.processEdit(change); |
| assert.equal(element.patchRange.patchNum, undefined); |
| |
| change.revisions['bar'] = { |
| _number: EDIT, |
| basePatchNum: 1 as BasePatchSetNum, |
| commit: { |
| ...createCommit(), |
| commit: 'bar' as CommitId, |
| }, |
| fetch: {}, |
| }; |
| |
| // When edit is set, but not patchNum, then switch to edit ps. |
| element.processEdit(change); |
| assert.equal(element.patchRange.patchNum, EDIT); |
| |
| // When edit is set, but patchNum as well, then keep patchNum. |
| element.patchRange.patchNum = 5 as RevisionPatchSetNum; |
| element.routerPatchNum = 5 as RevisionPatchSetNum; |
| element.processEdit(change); |
| assert.equal(element.patchRange.patchNum, 5 as RevisionPatchSetNum); |
| }); |
| |
| test('file-action-tap handling', async () => { |
| element.patchRange = { |
| basePatchNum: PARENT, |
| patchNum: 1 as RevisionPatchSetNum, |
| }; |
| element.change = { |
| ...createChangeViewChange(), |
| }; |
| assertIsDefined(element.fileList); |
| assertIsDefined(element.fileListHeader); |
| const fileList = element.fileList; |
| const Actions = GrEditConstants.Actions; |
| element.fileListHeader.editMode = true; |
| await element.fileListHeader.updateComplete; |
| await element.updateComplete; |
| const controls = queryAndAssert<GrEditControls>( |
| element.fileListHeader, |
| '#editControls' |
| ); |
| const openDeleteDialogStub = sinon.stub(controls, 'openDeleteDialog'); |
| const openRenameDialogStub = sinon.stub(controls, 'openRenameDialog'); |
| const openRestoreDialogStub = sinon.stub(controls, 'openRestoreDialog'); |
| |
| // Delete |
| fileList.dispatchEvent( |
| new CustomEvent('file-action-tap', { |
| detail: {action: Actions.DELETE.id, path: 'foo'}, |
| bubbles: true, |
| composed: true, |
| }) |
| ); |
| await element.updateComplete; |
| |
| assert.isTrue(openDeleteDialogStub.called); |
| assert.equal(openDeleteDialogStub.lastCall.args[0], 'foo'); |
| |
| // Restore |
| fileList.dispatchEvent( |
| new CustomEvent('file-action-tap', { |
| detail: {action: Actions.RESTORE.id, path: 'foo'}, |
| bubbles: true, |
| composed: true, |
| }) |
| ); |
| await element.updateComplete; |
| |
| assert.isTrue(openRestoreDialogStub.called); |
| assert.equal(openRestoreDialogStub.lastCall.args[0], 'foo'); |
| |
| // Rename |
| fileList.dispatchEvent( |
| new CustomEvent('file-action-tap', { |
| detail: {action: Actions.RENAME.id, path: 'foo'}, |
| bubbles: true, |
| composed: true, |
| }) |
| ); |
| await element.updateComplete; |
| |
| assert.isTrue(openRenameDialogStub.called); |
| assert.equal(openRenameDialogStub.lastCall.args[0], 'foo'); |
| |
| // Open |
| fileList.dispatchEvent( |
| new CustomEvent('file-action-tap', { |
| detail: {action: Actions.OPEN.id, path: 'foo'}, |
| bubbles: true, |
| composed: true, |
| }) |
| ); |
| await element.updateComplete; |
| |
| assert.isTrue(setUrlStub.called); |
| }); |
| |
| test('selectedRevision updates when patchNum is changed', async () => { |
| const revision1: RevisionInfo = createRevision(1); |
| const revision2: RevisionInfo = createRevision(2); |
| element.getChangeModel().setState({ |
| loadingStatus: LoadingStatus.LOADED, |
| change: { |
| ...createChangeViewChange(), |
| revisions: { |
| aaa: revision1, |
| bbb: revision2, |
| }, |
| labels: {}, |
| actions: {}, |
| current_revision: 'bbb' as CommitId, |
| }, |
| }); |
| element.userModel.setPreferences(createPreferences()); |
| |
| element.patchRange = {patchNum: 2 as RevisionPatchSetNum}; |
| await element.performPostChangeLoadTasks(); |
| assert.strictEqual(element.selectedRevision, revision2); |
| |
| element.patchRange = {patchNum: 1 as RevisionPatchSetNum}; |
| await element.updateComplete; |
| assert.strictEqual(element.selectedRevision, revision1); |
| }); |
| |
| test('selectedRevision is assigned when patchNum is edit', async () => { |
| const revision1 = createRevision(1); |
| const revision2 = createRevision(2); |
| const revision3 = createEditRevision(); |
| element.getChangeModel().setState({ |
| loadingStatus: LoadingStatus.LOADED, |
| change: { |
| ...createChangeViewChange(), |
| revisions: { |
| aaa: revision1, |
| bbb: revision2, |
| ccc: revision3, |
| }, |
| labels: {}, |
| actions: {}, |
| current_revision: 'ccc' as CommitId, |
| }, |
| }); |
| stubRestApi('getPreferences').returns(Promise.resolve(createPreferences())); |
| |
| element.patchRange = {patchNum: EDIT}; |
| await element.performPostChangeLoadTasks(); |
| assert.strictEqual(element.selectedRevision, revision3); |
| }); |
| |
| test('sendShowChangeEvent', () => { |
| const change = {...createChangeViewChange(), labels: {}}; |
| element.change = {...change}; |
| element.patchRange = {patchNum: 4 as RevisionPatchSetNum}; |
| element.mergeable = true; |
| const showStub = sinon.stub(element.jsAPI, 'handleEvent'); |
| element.sendShowChangeEvent(); |
| assert.isTrue(showStub.calledOnce); |
| assert.equal(showStub.lastCall.args[0], EventType.SHOW_CHANGE); |
| assert.deepEqual(showStub.lastCall.args[1], { |
| change, |
| patchNum: 4, |
| info: {mergeable: true}, |
| }); |
| }); |
| |
| test('patch range changed', () => { |
| element.patchRange = undefined; |
| element.change = createChangeViewChange(); |
| element.change.revisions = createRevisions(4); |
| element.change.current_revision = '1' as CommitId; |
| element.change = {...element.change}; |
| |
| const viewState = createChangeViewState(); |
| |
| assert.isFalse(element.hasPatchRangeChanged(viewState)); |
| assert.isFalse(element.hasPatchNumChanged(viewState)); |
| |
| viewState.basePatchNum = PARENT; |
| // undefined means navigate to latest patchset |
| viewState.patchNum = undefined; |
| |
| element.patchRange = { |
| patchNum: 2 as RevisionPatchSetNum, |
| basePatchNum: PARENT, |
| }; |
| |
| assert.isTrue(element.hasPatchRangeChanged(viewState)); |
| assert.isTrue(element.hasPatchNumChanged(viewState)); |
| |
| element.patchRange = { |
| patchNum: 4 as RevisionPatchSetNum, |
| basePatchNum: PARENT, |
| }; |
| |
| assert.isFalse(element.hasPatchRangeChanged(viewState)); |
| assert.isFalse(element.hasPatchNumChanged(viewState)); |
| }); |
| |
| suite('handleEditTap', () => { |
| let fireEdit: () => void; |
| |
| setup(() => { |
| fireEdit = () => { |
| assertIsDefined(element.actions); |
| element.actions.dispatchEvent(new CustomEvent('edit-tap')); |
| }; |
| |
| element.change = { |
| ...createChangeViewChange(), |
| revisions: {rev1: createRevision()}, |
| }; |
| }); |
| |
| test('edit exists in revisions', async () => { |
| assertIsDefined(element.change); |
| const newChange = {...element.change}; |
| newChange.revisions.rev2 = createRevision(EDIT); |
| element.change = newChange; |
| await element.updateComplete; |
| |
| fireEdit(); |
| assert.isTrue(setUrlStub.called); |
| assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42/edit'); |
| }); |
| |
| test('no edit exists in revisions, non-latest patchset', async () => { |
| assertIsDefined(element.change); |
| const newChange = {...element.change}; |
| newChange.revisions.rev2 = createRevision(2); |
| element.change = newChange; |
| element.routerPatchNum = 1 as RevisionPatchSetNum; |
| await element.updateComplete; |
| |
| fireEdit(); |
| assert.isTrue(setUrlStub.called); |
| assert.equal( |
| setUrlStub.lastCall.firstArg, |
| '/c/test-project/+/42/1,edit?forceReload=true' |
| ); |
| }); |
| |
| test('no edit exists in revisions, latest patchset', async () => { |
| assertIsDefined(element.change); |
| const newChange = {...element.change}; |
| newChange.revisions.rev2 = createRevision(2); |
| element.change = newChange; |
| element.patchRange = {patchNum: 2 as RevisionPatchSetNum}; |
| await element.updateComplete; |
| |
| fireEdit(); |
| assert.isTrue(setUrlStub.called); |
| assert.equal( |
| setUrlStub.lastCall.firstArg, |
| '/c/test-project/+/42,edit?forceReload=true' |
| ); |
| }); |
| }); |
| |
| test('handleStopEditTap', async () => { |
| element.change = { |
| ...createChangeViewChange(), |
| }; |
| await element.updateComplete; |
| assertIsDefined(element.metadata); |
| assertIsDefined(element.actions); |
| sinon.stub(element.metadata, 'computeLabelNames'); |
| |
| element.patchRange = {patchNum: 1 as RevisionPatchSetNum}; |
| element.actions.dispatchEvent( |
| new CustomEvent('stop-edit-tap', {bubbles: false}) |
| ); |
| |
| assert.isTrue(setUrlStub.called); |
| assert.equal( |
| setUrlStub.lastCall.firstArg, |
| '/c/test-project/+/42/1?forceReload=true' |
| ); |
| }); |
| |
| suite('plugin endpoints', () => { |
| test('endpoint params', async () => { |
| element.change = {...createChangeViewChange(), labels: {}}; |
| element.selectedRevision = createRevision(); |
| const promise = mockPromise(); |
| window.Gerrit.install( |
| promise.resolve, |
| '0.1', |
| 'http://some/plugins/url.js' |
| ); |
| await element.updateComplete; |
| const plugin: PluginApi = (await promise) as PluginApi; |
| const hookEl = await plugin |
| .hook('change-view-integration') |
| .getLastAttached(); |
| assert.strictEqual((hookEl as any).plugin, plugin); |
| assert.strictEqual((hookEl as any).change, element.change); |
| assert.strictEqual((hookEl as any).revision, element.selectedRevision); |
| }); |
| }); |
| |
| suite('getMergeability', () => { |
| let getMergeableStub: SinonStubbedMember<RestApiService['getMergeable']>; |
| setup(() => { |
| element.change = {...createChangeViewChange(), labels: {}}; |
| getMergeableStub = stubRestApi('getMergeable').returns( |
| Promise.resolve({...createMergeable(), mergeable: true}) |
| ); |
| }); |
| |
| test('merged change', () => { |
| element.mergeable = null; |
| element.change!.status = ChangeStatus.MERGED; |
| return element.getMergeability().then(() => { |
| assert.isFalse(element.mergeable); |
| assert.isFalse(getMergeableStub.called); |
| }); |
| }); |
| |
| test('abandoned change', () => { |
| element.mergeable = null; |
| element.change!.status = ChangeStatus.ABANDONED; |
| return element.getMergeability().then(() => { |
| assert.isFalse(element.mergeable); |
| assert.isFalse(getMergeableStub.called); |
| }); |
| }); |
| |
| test('open change', () => { |
| element.mergeable = null; |
| return element.getMergeability().then(() => { |
| assert.isTrue(element.mergeable); |
| assert.isTrue(getMergeableStub.called); |
| }); |
| }); |
| }); |
| |
| test('handleToggleStar called when star is tapped', async () => { |
| element.change = { |
| ...createChangeViewChange(), |
| owner: {_account_id: 1 as AccountId}, |
| starred: false, |
| }; |
| element.loggedIn = true; |
| await element.updateComplete; |
| |
| const stub = sinon.stub(element, 'handleToggleStar'); |
| |
| const changeStar = queryAndAssert<GrChangeStar>(element, '#changeStar'); |
| queryAndAssert<HTMLButtonElement>(changeStar, 'button')!.click(); |
| assert.isTrue(stub.called); |
| }); |
| |
| suite('gr-reporting tests', () => { |
| setup(() => { |
| element.patchRange = { |
| basePatchNum: PARENT, |
| patchNum: 1 as RevisionPatchSetNum, |
| }; |
| sinon |
| .stub(element, 'performPostChangeLoadTasks') |
| .returns(Promise.resolve(false)); |
| sinon.stub(element, 'getMergeability').returns(Promise.resolve()); |
| sinon.stub(element, 'getLatestCommitMessage').returns(Promise.resolve()); |
| sinon |
| .stub(element, 'reloadPatchNumDependentResources') |
| .returns(Promise.resolve([undefined, undefined, undefined])); |
| }); |
| |
| test("don't report changeDisplayed on reply", async () => { |
| const changeDisplayStub = sinon.stub( |
| element.reporting, |
| 'changeDisplayed' |
| ); |
| const changeFullyLoadedStub = sinon.stub( |
| element.reporting, |
| 'changeFullyLoaded' |
| ); |
| element.handleReplySent(); |
| await element.updateComplete; |
| assert.isFalse(changeDisplayStub.called); |
| assert.isFalse(changeFullyLoadedStub.called); |
| }); |
| |
| test('report changeDisplayed on viewStateChanged', async () => { |
| stubRestApi('getChangeOrEditFiles').resolves({ |
| 'a-file.js': {}, |
| }); |
| const changeDisplayStub = sinon.stub( |
| element.reporting, |
| 'changeDisplayed' |
| ); |
| const changeFullyLoadedStub = sinon.stub( |
| element.reporting, |
| 'changeFullyLoaded' |
| ); |
| // reset so reload is triggered |
| element.changeNum = undefined; |
| element.viewState = { |
| ...createChangeViewState(), |
| changeNum: TEST_NUMERIC_CHANGE_ID, |
| project: TEST_PROJECT_NAME, |
| }; |
| element.getChangeModel().setState({ |
| loadingStatus: LoadingStatus.LOADED, |
| change: { |
| ...createChangeViewChange(), |
| labels: {}, |
| current_revision: 'foo' as CommitId, |
| revisions: {foo: createRevision()}, |
| }, |
| }); |
| await element.updateComplete; |
| await waitEventLoop(); |
| assert.isTrue(changeDisplayStub.called); |
| assert.isTrue(changeFullyLoadedStub.called); |
| }); |
| }); |
| |
| test('calculateHasParent', () => { |
| const changeId = '123' as ChangeId; |
| const relatedChanges: RelatedChangeAndCommitInfo[] = []; |
| |
| assert.equal(element.calculateHasParent(changeId, relatedChanges), false); |
| |
| relatedChanges.push({ |
| ...createRelatedChangeAndCommitInfo(), |
| change_id: '123' as ChangeId, |
| }); |
| assert.equal(element.calculateHasParent(changeId, relatedChanges), false); |
| |
| relatedChanges.push({ |
| ...createRelatedChangeAndCommitInfo(), |
| change_id: '234' as ChangeId, |
| }); |
| assert.equal(element.calculateHasParent(changeId, relatedChanges), true); |
| }); |
| |
| test('renders sha in copy links', async () => { |
| stubFlags('isEnabled').returns(true); |
| const sha = '123' as CommitId; |
| element.change = { |
| ...createChangeViewChange(), |
| status: ChangeStatus.MERGED, |
| current_revision: sha, |
| }; |
| await element.updateComplete; |
| |
| const copyLinksDialog = queryAndAssert<GrCopyLinks>( |
| element, |
| 'gr-copy-links' |
| ); |
| assert.isTrue( |
| copyLinksDialog.copyLinks.some(copyLink => copyLink.value === sha) |
| ); |
| }); |
| |
| test('copy links without a base URL', async () => { |
| element.change = createChangeViewChange(); |
| await element.updateComplete; |
| |
| const copyLinksDialog = queryAndAssert<GrCopyLinks>( |
| element, |
| 'gr-copy-links' |
| ); |
| assert.deepEqual(copyLinksDialog.copyLinks[1], { |
| label: 'Change URL', |
| shortcut: 'u', |
| value: 'http://localhost:9876/c/test-project/+/42', |
| }); |
| }); |
| |
| test('copy links with a base URL having a path', async () => { |
| stubBaseUrl('/review'); |
| element.change = createChangeViewChange(); |
| await element.updateComplete; |
| |
| const copyLinksDialog = queryAndAssert<GrCopyLinks>( |
| element, |
| 'gr-copy-links' |
| ); |
| |
| assert.deepEqual(copyLinksDialog.copyLinks[1], { |
| label: 'Change URL', |
| shortcut: 'u', |
| value: 'http://localhost:9876/review/c/test-project/+/42', |
| }); |
| }); |
| }); |