<commit_before>/**
 * @license
 * Copyright 2016 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import '../../../test/common-test-setup';
import './gr-change-actions';
import {navigationToken} from '../../core/gr-navigation/gr-navigation';
import {
  createAccountWithId,
  createApproval,
  createChange,
  createChangeMessages,
  createChangeViewChange,
  createRevision,
  createRevisions,
} from '../../../test/test-data-generators';
import {ChangeStatus, HttpMethod} from '../../../constants/constants';
import {
  mockPromise,
  query,
  queryAll,
  queryAndAssert,
  stubReporting,
  stubRestApi,
} from '../../../test/test-utils';
import {assertUIActionInfo, GrChangeActions} from './gr-change-actions';
import {
  AccountId,
  ActionInfo,
  ActionNameToActionInfoMap,
  BranchName,
  ChangeId,
  ChangeSubmissionId,
  CommitId,
  NumericChangeId,
  PatchSetNum,
  PatchSetNumber,
  RepoName,
  ReviewInput,
  TopicName,
} from '../../../types/common';
import {ActionType} from '../../../api/change-actions';
import {SinonFakeTimers} from 'sinon';
import {IronAutogrowTextareaElement} from '@polymer/iron-autogrow-textarea';
import {GrButton} from '../../shared/gr-button/gr-button';
import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
import {UIActionInfo} from '../../shared/gr-js-api-interface/gr-change-actions-js-api';
import {fixture, html, assert} from '@open-wc/testing';
import {GrConfirmCherrypickDialog} from '../gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog';
import {GrDropdown} from '../../shared/gr-dropdown/gr-dropdown';
import {GrConfirmSubmitDialog} from '../gr-confirm-submit-dialog/gr-confirm-submit-dialog';
import {GrConfirmRebaseDialog} from '../gr-confirm-rebase-dialog/gr-confirm-rebase-dialog';
import {GrConfirmMoveDialog} from '../gr-confirm-move-dialog/gr-confirm-move-dialog';
import {GrConfirmAbandonDialog} from '../gr-confirm-abandon-dialog/gr-confirm-abandon-dialog';
import {GrConfirmRevertDialog} from '../gr-confirm-revert-dialog/gr-confirm-revert-dialog';
import {EventType} from '../../../types/events';
import {testResolver} from '../../../test/common-test-setup';
import {storageServiceToken} from '../../../services/storage/gr-storage_impl';
import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';

// TODO(dhruvsri): remove use of _populateRevertMessage as it's private
suite('gr-change-actions tests', () => {
  let element: GrChangeActions;

  suite('basic tests', () => {
    setup(async () => {
      stubRestApi('getChangeRevisionActions').returns(
        Promise.resolve({
          cherrypick: {
            method: HttpMethod.POST,
            label: 'Cherry Pick',
            title: 'Cherry pick change to a different branch',
            enabled: true,
          },
          rebase: {
            method: HttpMethod.POST,
            label: 'Rebase',
            title: 'Rebase onto tip of branch or parent change',
            enabled: true,
          },
          submit: {
            method: HttpMethod.POST,
            label: 'Submit',
            title: 'Submit patch set 2 into master',
            enabled: true,
          },
          revert_submission: {
            method: HttpMethod.POST,
            label: 'Revert submission',
            title: 'Revert this submission',
            enabled: true,
          },
        })
      );
      stubRestApi('send').callsFake((method, url) => {
        if (method !== 'POST') {
          return Promise.reject(new Error('bad method'));
        }
        if (url === '/changes/test~42/revisions/2/submit') {
          return Promise.resolve({
            ...new Response(),
            ok: true,
            text() {
              return Promise.resolve(")]}'\n{}");
            },
          });
        } else if (url === '/changes/test~42/revisions/2/rebase') {
          return Promise.resolve({
            ...new Response(),
            ok: true,
            text() {
              return Promise.resolve(")]}'\n{}");
            },
          });
        }
        return Promise.reject(new Error('bad url'));
      });

      sinon
        .stub(testResolver(pluginLoaderToken), 'awaitPluginsLoaded')
        .returns(Promise.resolve());

      element = await fixture<GrChangeActions>(html`
        <gr-change-actions></gr-change-actions>
      `);
      element.change = {
        ...createChangeViewChange(),
        actions: {
          '/': {
            method: HttpMethod.DELETE,
            label: 'Delete Change',
            title: 'Delete change X_X',
            enabled: true,
          },
        },
      };
      element.changeNum = 42 as NumericChangeId;
      element.latestPatchNum = 2 as PatchSetNum;
      element.account = {
        _account_id: 123 as AccountId,
      };
      stubRestApi('getRepoBranches').returns(Promise.resolve([]));

      await element.updateComplete;
      await element.reload();
    });

    test('render', () => {
      assert.shadowDom.equal(
        element,
        /* HTML */ `
          <div id="mainContent">
            <span hidden="" id="actionLoadingMessage"> </span>
            <section id="primaryActions">
              <gr-tooltip-content
                has-tooltip=""
                position-below=""
                title="Submit patch set 2 into master"
              >
                <gr-button
                  aria-disabled="false"
                  class="submit"
                  data-action-key="submit"
                  data-label="Submit"
                  link=""
                  role="button"
                  tabindex="0"
                >
                  <gr-icon icon="done_all"></gr-icon>
                  Submit
                </gr-button>
              </gr-tooltip-content>
            </section>
            <section id="secondaryActions">
              <gr-tooltip-content
                has-tooltip=""
                position-below=""
                title="Rebase onto tip of branch or parent change"
              >
                <gr-button
                  aria-disabled="true"
                  class="rebase"
                  data-action-key="rebase"
                  data-label="Rebase"
                  disabled=""
                  link=""
                  role="button"
                  tabindex="-1"
                >
                  <gr-icon icon="rebase"> </gr-icon>
                  Rebase
                </gr-button>
              </gr-tooltip-content>
            </section>
            <gr-button
              aria-disabled="false"
              hidden=""
              role="button"
              tabindex="0"
            >
              Loading actions...
            </gr-button>
            <gr-dropdown id="moreActions" link="">
              <gr-icon icon="more_vert" aria-labelledby="moreMessage"></gr-icon>
              <span id="moreMessage"> More </span>
            </gr-dropdown>
          </div>
          <dialog id="actionsModal" tabindex="-1">
            <gr-confirm-rebase-dialog class="confirmDialog" id="confirmRebase">
            </gr-confirm-rebase-dialog>
            <gr-confirm-cherrypick-dialog
              class="confirmDialog"
              id="confirmCherrypick"
            >
            </gr-confirm-cherrypick-dialog>
            <gr-confirm-cherrypick-conflict-dialog
              class="confirmDialog"
              id="confirmCherrypickConflict"
            >
            </gr-confirm-cherrypick-conflict-dialog>
            <gr-confirm-move-dialog class="confirmDialog" id="confirmMove">
            </gr-confirm-move-dialog>
            <gr-confirm-revert-dialog
              class="confirmDialog"
              id="confirmRevertDialog"
            >
            </gr-confirm-revert-dialog>
            <gr-confirm-abandon-dialog
              class="confirmDialog"
              id="confirmAbandonDialog"
            >
            </gr-confirm-abandon-dialog>
            <gr-confirm-submit-dialog
              class="confirmDialog"
              id="confirmSubmitDialog"
            >
            </gr-confirm-submit-dialog>
            <gr-dialog
              class="confirmDialog"
              confirm-label="Create"
              id="createFollowUpDialog"
              role="dialog"
            >
              <div class="header" slot="header">Create Follow-Up Change</div>
              <div class="main" slot="main">
                <gr-create-change-dialog id="createFollowUpChange">
                </gr-create-change-dialog>
              </div>
            </gr-dialog>
            <gr-dialog
              class="confirmDialog"
              confirm-label="Delete"
              confirm-on-enter=""
              id="confirmDeleteDialog"
              role="dialog"
            >
              <div class="header" slot="header">Delete Change</div>
              <div class="main" slot="main">
                Do you really want to delete the change?
              </div>
            </gr-dialog>
            <gr-dialog
              class="confirmDialog"
              confirm-label="Delete"
              confirm-on-enter=""
              id="confirmDeleteEditDialog"
              role="dialog"
            >
              <div class="header" slot="header">Delete Change Edit</div>
              <div class="main" slot="main">
                Do you really want to delete the edit?
              </div>
            </gr-dialog>
          </dialog>
        `
      );
    });

    test('show-revision-actions event should fire', async () => {
      const spy = sinon.spy(element, 'sendShowRevisionActions');
      element.reload();
      await element.updateComplete;
      assert.isTrue(spy.called);
    });

    test('primary and secondary actions split properly', () => {
      // Submit should be the only primary action.
      assert.equal(element.topLevelPrimaryActions!.length, 1);
      assert.equal(element.topLevelPrimaryActions![0].label, 'Submit');
      assert.equal(
        element.topLevelSecondaryActions!.length,
        element.topLevelActions!.length - 1
      );
    });

    test('revert submission action is skipped', () => {
      assert.equal(
        element.allActionValues.filter(action => action.__key === 'submit')
          .length,
        1
      );
      assert.equal(
        element.allActionValues.filter(
          action => action.__key === 'revert_submission'
        ).length,
        0
      );
    });

    test('plugin revision actions', async () => {
      const stub = stubRestApi('getChangeActionURL').returns(
        Promise.resolve('the-url')
      );
      element.revisionActions = {
        'plugin~action': {},
      };
      assert.isOk(element.revisionActions['plugin~action']);
      await element.updateComplete;
      assert.isTrue(
        stub.calledWith(
          element.changeNum,
          element.latestPatchNum,
          '/plugin~action'
        )
      );
      assert.equal(
        (element.revisionActions['plugin~action'] as UIActionInfo)!.__url,
        'the-url'
      );
    });

    test('plugin change actions', async () => {
      const stub = stubRestApi('getChangeActionURL').returns(
        Promise.resolve('the-url')
      );
      element.actions = {
        'plugin~action': {},
      };
      assert.isOk(element.actions['plugin~action']);
      await element.updateComplete;
      assert.isTrue(
        stub.calledWith(element.changeNum, undefined, '/plugin~action')
      );
      assert.equal(
        (element.actions['plugin~action'] as UIActionInfo)!.__url,
        'the-url'
      );
    });

    test('not supported actions are filtered out', () => {
      element.revisionActions = {followup: {}};
      assert.equal(
        element.querySelectorAll(
          'section gr-button[data-action-type="revision"]'
        ).length,
        0
      );
    });

    test('getActionDetails', () => {
      element.revisionActions = {
        'plugin~action': {},
        ...element.revisionActions,
      };
      assert.isUndefined(element.getActionDetails('rubbish'));
      assert.strictEqual(
        element.revisionActions['plugin~action'],
        element.getActionDetails('plugin~action')
      );
      assert.strictEqual(
        element.revisionActions['rebase'],
        element.getActionDetails('rebase')
      );
    });

    test('hide revision action', async () => {
      await element.updateComplete;
      let buttonEl: Element | undefined = queryAndAssert(
        element,
        '[data-action-key="submit"]'
      );
      assert.isOk(buttonEl);
      element.setActionHidden(
        element.ActionType.REVISION,
        element.RevisionActions.SUBMIT,
        true
      );
      assert.lengthOf(element.hiddenActions, 1);
      await element.updateComplete;
      buttonEl = query(element, '[data-action-key="submit"]');
      assert.isNotOk(buttonEl);

      element.setActionHidden(
        element.ActionType.REVISION,
        element.RevisionActions.SUBMIT,
        false
      );
      await element.updateComplete;
      buttonEl = queryAndAssert(element, '[data-action-key="submit"]');
      assert.isFalse(buttonEl.hasAttribute('hidden'));
    });

    test('buttons exist', async () => {
      element.loading = false;
      await element.updateComplete;
      const buttonEls = queryAll(element, 'gr-button');
      const menuItems = queryAndAssert<GrDropdown>(
        element,
        '#moreActions'
      ).items;

      // Total button number is one greater than the number of total actions
      // due to the existence of the overflow menu trigger.
      assert.equal(
        buttonEls.length + menuItems!.length,
        element.allActionValues.length + 1
      );
      assert.isFalse(element.hidden);
    });

    test('delete buttons have explicit labels', async () => {
      await element.updateComplete;
      const deleteItems = queryAndAssert<GrDropdown>(
        element,
        '#moreActions'
      ).items!.filter(item => item.id!.startsWith('delete'));
      assert.equal(deleteItems.length, 1);
      assert.equal(deleteItems[0].name, 'Delete change');
    });

    test('get revision object from change', () => {
      const revObj = {
        ...createRevision(2),
        foo: 'bar',
      };
      const change = {
        ...createChangeViewChange(),
        revisions: {
          rev1: createRevision(1),
          rev2: revObj,
        },
      };
      assert.deepEqual(
        element.getRevision(change, 2 as PatchSetNumber),
        revObj
      );
    });

    test('actionComparator sort order', () => {
      const actions = [
        {label: '123', __type: ActionType.CHANGE, __key: 'review'},
        {label: 'abc-ro', __type: ActionType.REVISION, __key: 'random'},
        {label: 'abc', __type: ActionType.CHANGE, __key: 'random'},
        {label: 'def', __type: ActionType.CHANGE, __key: 'random'},
        {
          label: 'def-p',
          __type: ActionType.CHANGE,
          __primary: true,
          __key: 'random',
        },
      ];

      const result = actions.slice();
      result.reverse();
      result.sort(element.actionComparator.bind(element));
      assert.deepEqual(result, actions);
    });

    test('submit change', async () => {
      const showSpy = sinon.spy(element, 'showActionDialog');
      stubRestApi('getFromProjectLookup').returns(
        Promise.resolve('test' as RepoName)
      );
      element.change = {
        ...createChangeViewChange(),
        revisions: {
          rev1: {...createRevision(), _number: 1 as PatchSetNumber},
          rev2: {...createRevision(), _number: 2 as PatchSetNumber},
        },
      };
      element.latestPatchNum = 2 as PatchSetNumber;

      queryAndAssert<GrButton>(
        element,
        'gr-button[data-action-key="submit"]'
      ).click();

      await element.updateComplete;
      assert.isTrue(
        showSpy.calledWith(
          queryAndAssert<GrConfirmSubmitDialog>(element, '#confirmSubmitDialog')
        )
      );
    });

    test('submit change, tap on icon', async () => {
      const submitted = mockPromise();
      sinon
        .stub(
          queryAndAssert<GrConfirmSubmitDialog>(
            element,
            '#confirmSubmitDialog'
          ),
          'resetFocus'
        )
        .callsFake(() => submitted.resolve());
      stubRestApi('getFromProjectLookup').returns(
        Promise.resolve('test' as RepoName)
      );
      element.change = {
        ...createChangeViewChange(),
        revisions: {
          rev1: {...createRevision(), _number: 1 as PatchSetNumber},
          rev2: {...createRevision(), _number: 2 as PatchSetNumber},
        },
      };
      element.latestPatchNum = 2 as PatchSetNum;

      queryAndAssert<GrButton>(
        element,
        'gr-button[data-action-key="submit"] gr-icon'
      ).click();
      await submitted;
    });

    test('correct icons', async () => {
      element.loggedIn = true;
      await element.updateComplete;

      queryAndAssert<GrButton>(
        element,
        'gr-button[data-action-key="submit"] gr-icon'
      );
      queryAndAssert<GrButton>(
        element,
        'gr-button[data-action-key="rebase"] gr-icon'
      );
      queryAndAssert<GrButton>(
        element,
        'gr-button[data-action-key="edit"] gr-icon[filled]'
      );
    });

    test('handleSubmitConfirm', () => {
      const fireStub = sinon.stub(element, 'fireAction');
      sinon.stub(element, 'canSubmitChange').returns(true);
      element.handleSubmitConfirm();
      assert.isTrue(fireStub.calledOnce);
      assert.deepEqual(fireStub.lastCall.args, [
        '/submit',
        assertUIActionInfo(element.revisionActions.submit),
        true,
      ]);
    });

    test('handleSubmitConfirm when not able to submit', () => {
      const fireStub = sinon.stub(element, 'fireAction');
      sinon.stub(element, 'canSubmitChange').returns(false);
      element.handleSubmitConfirm();
      assert.isFalse(fireStub.called);
    });

    test('submit change with plugin hook', async () => {
      sinon.stub(element, 'canSubmitChange').callsFake(() => false);
      const fireActionStub = sinon.stub(element, 'fireAction');
      await element.updateComplete;
      queryAndAssert<GrButton>(
        element,
        'gr-button[data-action-key="submit"]'
      ).click();
      assert.equal(fireActionStub.callCount, 0);
    });

    test('chain state', async () => {
      assert.equal(element._hasKnownChainState, false);
      element.hasParent = true;
      await element.updateComplete;
      assert.equal(element._hasKnownChainState, true);
    });

    test('calculateDisabled', () => {
      const action = {
        __key: 'rebase',
        enabled: true,
        __type: ActionType.CHANGE,
        label: 'l',
      };
      element._hasKnownChainState = false;
      assert.equal(element.calculateDisabled(action), true);

      action.__key = 'delete';
      assert.equal(element.calculateDisabled(action), false);

      action.__key = 'rebase';
      element._hasKnownChainState = true;
      assert.equal(element.calculateDisabled(action), false);

      action.enabled = false;
      assert.equal(element.calculateDisabled(action), false);
    });

    test('rebase change', async () => {
      const fireActionStub = sinon.stub(element, 'fireAction');
      const fetchChangesStub = sinon
        .stub(
          queryAndAssert<GrConfirmRebaseDialog>(element, '#confirmRebase'),
          'fetchRecentChanges'
        )
        .returns(Promise.resolve([]));
      element._hasKnownChainState = true;
      await element.updateComplete;
      queryAndAssert<GrButton>(
        element,
        'gr-button[data-action-key="rebase"]'
      ).click();
      const rebaseAction = {
        __key: 'rebase',
        __type: 'revision',
        __primary: false,
        enabled: true,
        label: 'Rebase',
        method: HttpMethod.POST,
        title: 'Rebase onto tip of branch or parent change',
      };
      assert.isTrue(fetchChangesStub.called);
      element.handleRebaseConfirm(
        new CustomEvent('', {detail: {base: '1234', allowConflicts: false}})
      );
      assert.deepEqual(fireActionStub.lastCall.args, [
        '/rebase',
        assertUIActionInfo(rebaseAction),
        true,
        {base: '1234', allow_conflicts: false},
        {allow_conflicts: false},
      ]);
    });

    test('rebase change fires reload event', async () => {
      const eventStub = sinon.stub(element, 'dispatchEvent');
      await element.handleResponse(
        {__key: 'rebase', __type: ActionType.CHANGE, label: 'l'},
        new Response()
      );
      assert.isTrue(eventStub.called);
      assert.equal(eventStub.lastCall.args[0].type, 'reload');
    });

    test("rebase dialog gets recent changes each time it's opened", async () => {
      const fetchChangesStub = sinon
        .stub(
          queryAndAssert<GrConfirmRebaseDialog>(element, '#confirmRebase'),
          'fetchRecentChanges'
        )
        .returns(Promise.resolve([]));
      element._hasKnownChainState = true;
      await element.updateComplete;
      const rebaseButton = queryAndAssert<GrButton>(
        element,
        'gr-button[data-action-key="rebase"]'
      );
      rebaseButton.click();
      await element.updateComplete;
      assert.isTrue(fetchChangesStub.calledOnce);

      await element.updateComplete;
      queryAndAssert<GrConfirmRebaseDialog>(
        element,
        '#confirmRebase'
      ).dispatchEvent(
        new CustomEvent('cancel', {
          composed: true,
          bubbles: true,
        })
      );
      rebaseButton.click();
      assert.isTrue(fetchChangesStub.calledTwice);
    });

    test('two dialogs are not shown at the same time', async () => {
      element._hasKnownChainState = true;
      await element.updateComplete;
      queryAndAssert<GrButton>(
        element,
        'gr-button[data-action-key="rebase"]'
      ).click();
      await element.updateComplete;
      assert.isFalse(
        queryAndAssert<GrConfirmRebaseDialog>(element, '#confirmRebase').hidden
      );
      stubRestApi('getChanges').returns(Promise.resolve([]));
      element.handleCherrypickTap();
      await element.updateComplete;
      assert.isTrue(
        queryAndAssert<GrConfirmRebaseDialog>(element, '#confirmRebase').hidden
      );
      assert.isFalse(
        queryAndAssert<GrConfirmCherrypickDialog>(element, '#confirmCherrypick')
          .hidden
      );
    });

    test('setReviewOnRevert', () => {
      const review = {labels: {Foo: 1, 'Bar-Baz': -2}};
      const changeId = 1234 as NumericChangeId;
      sinon
        .stub(
          testResolver(pluginLoaderToken).jsApiService,
          'getReviewPostRevert'
        )
        .returns(review);
      const saveStub = stubRestApi('saveChangeReview').returns(
        Promise.resolve(new Response())
      );
      const setReviewOnRevert = element.setReviewOnRevert(changeId) as Promise<
        undefined | Response
      >;
      return setReviewOnRevert.then((_res: Response | undefined) => {
        assert.isTrue(saveStub.calledOnce);
        assert.equal(saveStub.lastCall.args[0], changeId);
        assert.deepEqual(saveStub.lastCall.args[2], review);
      });
    });

    suite('change edits', () => {
      test('disableEdit', async () => {
        element.editMode = false;
        element.editBasedOnCurrentPatchSet = false;
        element.change = {
          ...createChangeViewChange(),
          status: ChangeStatus.NEW,
        };
        element.disableEdit = true;
        await element.updateComplete;

        assert.isNotOk(
          query(element, 'gr-button[data-action-key="publishEdit"]')
        );
        assert.isNotOk(
          query(element, 'gr-button[data-action-key="rebaseEdit"]')
        );
        assert.isNotOk(
          query(element, 'gr-button[data-action-key="deleteEdit"]')
        );
        assert.isNotOk(query(element, 'gr-button[data-action-key="edit"]'));
        assert.isNotOk(query(element, 'gr-button[data-action-key="stopEdit"]'));
      });

      test('shows confirm dialog for delete edit', async () => {
        element.loggedIn = true;
        element.editMode = true;
        element.editPatchsetLoaded = true;
        await element.updateComplete;

        const fireActionStub = sinon.stub(element, 'fireAction');
        element.handleDeleteEditTap();
        assert.isFalse(
          queryAndAssert<GrDialog>(element, '#confirmDeleteEditDialog').hidden
        );
        queryAndAssert<GrButton>(
          queryAndAssert(element, '#confirmDeleteEditDialog'),
          'gr-button[primary]'
        ).click();
        await element.updateComplete;

        assert.equal(fireActionStub.lastCall.args[0], '/edit');
      });

      test('all cached change edits get deleted on delete edit', async () => {
        element.loggedIn = true;
        element.editMode = true;
        element.editPatchsetLoaded = true;
        await element.updateComplete;

        const storage = testResolver(storageServiceToken);
        storage.setEditableContentItem(
          'c42_ps2_index.php',
          '<?php\necho 42_ps_2'
        );
        storage.setEditableContentItem(
          'c42_psedit_index.php',
          '<?php\necho 42_ps_edit'
        );

        assert.equal(
          storage.getEditableContentItem('c42_ps2_index.php')!.message,
          '<?php\necho 42_ps_2'
        );
        assert.equal(
          storage.getEditableContentItem('c42_psedit_index.php')!.message,
          '<?php\necho 42_ps_edit'
        );

        assert.isOk(storage.getEditableContentItem('c42_psedit_index.php')!);
        assert.isOk(storage.getEditableContentItem('c42_ps2_index.php')!);
        assert.isNotOk(storage.getEditableContentItem('c50_psedit_index.php')!);

        const eraseEditableContentItemsForChangeEditSpy = sinon.spy(
          storage,
          'eraseEditableContentItemsForChangeEdit'
        );
        sinon.stub(element, 'fireAction');
        element.handleDeleteEditTap();
        assert.isFalse(
          queryAndAssert<GrDialog>(element, '#confirmDeleteEditDialog').hidden
        );
        queryAndAssert<GrButton>(
          queryAndAssert(element, '#confirmDeleteEditDialog'),
          'gr-button[primary]'
        ).click();
        await element.updateComplete;
        assert.isTrue(eraseEditableContentItemsForChangeEditSpy.called);
        assert.isNotOk(storage.getEditableContentItem('c42_psedit_index.php')!);
        assert.isNotOk(storage.getEditableContentItem('c42_ps2_index.php')!);
      });

      test('edit patchset is loaded, needs rebase', async () => {
        element.loggedIn = true;
        element.editMode = true;
        element.editPatchsetLoaded = true;
        element.change = {
          ...createChangeViewChange(),
          status: ChangeStatus.NEW,
        };
        element.editBasedOnCurrentPatchSet = false;
        await element.updateComplete;

        assert.isNotOk(
          query(element, 'gr-button[data-action-key="publishEdit"]')
        );
        assert.isOk(query(element, 'gr-button[data-action-key="rebaseEdit"]'));
        assert.isOk(query(element, 'gr-button[data-action-key="deleteEdit"]'));
        assert.isNotOk(query(element, 'gr-button[data-action-key="edit"]'));
        assert.isNotOk(query(element, 'gr-button[data-action-key="stopEdit"]'));
      });

      test('edit patchset is loaded, does not need rebase', async () => {
        element.loggedIn = true;
        element.editMode = true;
        element.editPatchsetLoaded = true;
        element.change = {
          ...createChangeViewChange(),
          status: ChangeStatus.NEW,
        };
        element.editBasedOnCurrentPatchSet = true;
        await element.updateComplete;

        assert.isOk(query(element, 'gr-button[data-action-key="publishEdit"]'));
        assert.isNotOk(
          query(element, 'gr-button[data-action-key="rebaseEdit"]')
        );
        assert.isOk(query(element, 'gr-button[data-action-key="deleteEdit"]'));
        assert.isNotOk(query(element, 'gr-button[data-action-key="edit"]'));
        assert.isNotOk(query(element, 'gr-button[data-action-key="stopEdit"]'));
      });

      test('edit mode is loaded, no edit patchset', async () => {
        element.loggedIn = true;
        element.editMode = true;
        element.editPatchsetLoaded = false;
        element.change = {
          ...createChangeViewChange(),
          status: ChangeStatus.NEW,
        };
        await element.updateComplete;

        assert.isNotOk(
          query(element, 'gr-button[data-action-key="publishEdit"]')
        );
        assert.isNotOk(
          query(element, 'gr-button[data-action-key="rebaseEdit"]')
        );
        assert.isNotOk(
          query(element, 'gr-button[data-action-key="deleteEdit"]')
        );
        assert.isNotOk(query(element, 'gr-button[data-action-key="edit"]'));
        assert.isOk(query(element, 'gr-button[data-action-key="stopEdit"]'));
      });

      test('normal patch set', async () => {
        element.loggedIn = true;
        element.editMode = false;
        element.editPatchsetLoaded = false;
        element.change = {
          ...createChangeViewChange(),
          status: ChangeStatus.NEW,
        };
        await element.updateComplete;

        assert.isNotOk(
          query(element, 'gr-button[data-action-key="publishEdit"]')
        );
        assert.isNotOk(
          query(element, 'gr-button[data-action-key="rebaseEdit"]')
        );
        assert.isNotOk(
          query(element, 'gr-button[data-action-key="deleteEdit"]')
        );
        assert.isOk(query(element, 'gr-button[data-action-key="edit"]'));
        assert.isNotOk(query(element, 'gr-button[data-action-key="stopEdit"]'));
      });

      test('edit action', async () => {
        element.loggedIn = true;
        const editTapped = mockPromise();
        element.addEventListener('edit-tap', () => {
          editTapped.resolve();
        });
        element.editMode = true;
        element.change = {
          ...createChangeViewChange(),
          status: ChangeStatus.NEW,
        };
        await element.updateComplete;

        assert.isNotOk(query(element, 'gr-button[data-action-key="edit"]'));
        assert.isOk(query(element, 'gr-button[data-action-key="stopEdit"]'));
        element.change = {
          ...createChangeViewChange(),
          status: ChangeStatus.MERGED,
        };
        await element.updateComplete;

        assert.isNotOk(query(element, 'gr-button[data-action-key="edit"]'));
        element.change = {
          ...createChangeViewChange(),
          status: ChangeStatus.NEW,
        };
        element.editMode = false;
        await element.updateComplete;

        queryAndAssert<GrButton>(
          element,
          'gr-button[data-action-key="edit"]'
        ).click();
        await editTapped;
      });
    });

    test('edit action not shown for logged out user', async () => {
      element.loggedIn = false;
      element.editMode = false;
      element.editPatchsetLoaded = false;
      element.change = {
        ...createChangeViewChange(),
        status: ChangeStatus.NEW,
      };
      await element.updateComplete;

      assert.isNotOk(
        query(element, 'gr-button[data-action-key="publishEdit"]')
      );
      assert.isNotOk(query(element, 'gr-button[data-action-key="rebaseEdit"]'));
      assert.isNotOk(query(element, 'gr-button[data-action-key="deleteEdit"]'));
      assert.isNotOk(query(element, 'gr-button[data-action-key="edit"]'));
      assert.isNotOk(query(element, 'gr-button[data-action-key="stopEdit"]'));
    });

    suite('cherry-pick', () => {
      let fireActionStub: sinon.SinonStub;

      setup(() => {
        fireActionStub = sinon.stub(element, 'fireAction');
        sinon.stub(window, 'alert');
      });

      test('works', async () => {
        element.handleCherrypickTap();
        const action = {
          __key: 'cherrypick',
          __type: 'revision',
          __primary: false,
          enabled: true,
          label: 'Cherry pick',
          method: HttpMethod.POST,
          title: 'Cherry pick change to a different branch',
        };

        element.handleCherrypickConfirm();
        assert.equal(fireActionStub.callCount, 0);

        queryAndAssert<GrConfirmCherrypickDialog>(
          element,
          '#confirmCherrypick'
        ).branch = 'master' as BranchName;
        element.handleCherrypickConfirm();
        assert.equal(fireActionStub.callCount, 0); // Still needs a message.

        // Add attributes that are used to determine the message.
        queryAndAssert<GrConfirmCherrypickDialog>(
          element,
          '#confirmCherrypick'
        ).commitMessage = 'foo message';
        queryAndAssert<GrConfirmCherrypickDialog>(
          element,
          '#confirmCherrypick'
        ).changeStatus = ChangeStatus.NEW;
        queryAndAssert<GrConfirmCherrypickDialog>(
          element,
          '#confirmCherrypick'
        ).commitNum = '123' as CommitId;
        await element.updateComplete;

        element.handleCherrypickConfirm();
        await element.updateComplete;

        const autogrowEl = queryAndAssert<IronAutogrowTextareaElement>(
          queryAndAssert<GrConfirmCherrypickDialog>(
            element,
            '#confirmCherrypick'
          ),
          '#messageInput'
        );
        assert.equal(autogrowEl.value, 'foo message');

        assert.deepEqual(fireActionStub.lastCall.args, [
          '/cherrypick',
          action,
          true,
          {
            destination: 'master',
            base: null,
            message: 'foo message',
            allow_conflicts: false,
          },
        ]);
      });

      test('cherry pick even with conflicts', async () => {
        element.handleCherrypickTap();
        const action = {
          __key: 'cherrypick',
          __type: 'revision',
          __primary: false,
          enabled: true,
          label: 'Cherry pick',
          method: HttpMethod.POST,
          title: 'Cherry pick change to a different branch',
        };

        queryAndAssert<GrConfirmCherrypickDialog>(
          element,
          '#confirmCherrypick'
        ).branch = 'master' as BranchName;

        // Add attributes that are used to determine the message.
        queryAndAssert<GrConfirmCherrypickDialog>(
          element,
          '#confirmCherrypick'
        ).commitMessage = 'foo message';
        queryAndAssert<GrConfirmCherrypickDialog>(
          element,
          '#confirmCherrypick'
        ).changeStatus = ChangeStatus.NEW;
        queryAndAssert<GrConfirmCherrypickDialog>(
          element,
          '#confirmCherrypick'
        ).commitNum = '123' as CommitId;
        await element.updateComplete;

        element.handleCherrypickConflictConfirm();
        await element.updateComplete;

        assert.deepEqual(fireActionStub.lastCall.args, [
          '/cherrypick',
          action,
          true,
          {
            destination: 'master',
            base: null,
            message: 'foo message',
            allow_conflicts: true,
          },
        ]);
      });

      test('branch name cleared when re-open cherrypick', () => {
        const emptyBranchName = '';
        queryAndAssert<GrConfirmCherrypickDialog>(
          element,
          '#confirmCherrypick'
        ).branch = 'master' as BranchName;

        element.handleCherrypickTap();
        assert.equal(
          queryAndAssert<GrConfirmCherrypickDialog>(
            element,
            '#confirmCherrypick'
          ).branch,
          emptyBranchName
        );
      });

      suite('cherry pick topics', () => {
        const changes = [
          {
            ...createChangeViewChange(),
            change_id: '12345678901234' as ChangeId,
            topic: 'T' as TopicName,
            subject: 'random',
            project: 'A' as RepoName,
            status: ChangeStatus.MERGED,
          },
          {
            ...createChangeViewChange(),
            change_id: '23456' as ChangeId,
            topic: 'T' as TopicName,
            subject: 'a'.repeat(100),
            project: 'B' as RepoName,
            status: ChangeStatus.NEW,
          },
        ];
        setup(async () => {
          stubRestApi('getChanges').returns(Promise.resolve(changes));
          element.handleCherrypickTap();
          await element.updateComplete;
          const confirmCherrypick = queryAndAssert<GrConfirmCherrypickDialog>(
            element,
            '#confirmCherrypick'
          );
          await element.updateComplete;
          const radioButtons = queryAll<HTMLInputElement>(
            confirmCherrypick,
            "input[name='cherryPickOptions']"
          );
          assert.equal(radioButtons.length, 2);
          radioButtons[1].click();
          await element.updateComplete;
        });

        test('cherry pick topic dialog is rendered', async () => {
          const dialog = queryAndAssert<GrConfirmCherrypickDialog>(
            element,
            '#confirmCherrypick'
          );
          await element.updateComplete;
          const changesTable = queryAndAssert(dialog, 'table');
          const headers = Array.from(changesTable.querySelectorAll('th'));
          const expectedHeadings = [
            '',
            'Change',
            'Status',
            'Subject',
            'Project',
            'Progress',
            '',
          ];
          const headings = headers.map(header => header.innerText);
          assert.equal(headings.length, expectedHeadings.length);
          for (let i = 0; i < headings.length; i++) {
            assert.equal(headings[i].trim(), expectedHeadings[i]);
          }
          const changeRows = queryAll(changesTable, 'tbody > tr');
          const change = Array.from(changeRows[0].querySelectorAll('td')).map(
            e => e.innerText
          );
          const expectedChange = [
            '',
            '1234567890',
            'MERGED',
            'random',
            'A',
            'NOT STARTED',
            '',
          ];
          for (let i = 0; i < change.length; i++) {
            assert.equal(change[i].trim(), expectedChange[i]);
          }
        });

        test('changes with duplicate project show an error', async () => {
          const dialog = queryAndAssert<GrConfirmCherrypickDialog>(
            element,
            '#confirmCherrypick'
          );
          const error = queryAndAssert<HTMLSpanElement>(
            dialog,
            '.error-message'
          );
          assert.equal(error.innerText, '');
          dialog.updateChanges([
            {
              ...createChangeViewChange(),
              change_id: '12345678901234' as ChangeId,
              topic: 'T' as TopicName,
              subject: 'random',
              project: 'A' as RepoName,
            },
            {
              ...createChangeViewChange(),
              change_id: '23456' as ChangeId,
              topic: 'T' as TopicName,
              subject: 'a'.repeat(100),
              project: 'A' as RepoName,
            },
          ]);
          await element.updateComplete;
          assert.equal(
            error.innerText,
            'Two changes cannot be of the same' + ' project'
          );
        });
      });
    });

    suite('move change', () => {
      let fireActionStub: sinon.SinonStub;

      setup(async () => {
        fireActionStub = sinon.stub(element, 'fireAction');
        sinon.stub(window, 'alert');
        element.actions = {
          move: {
            method: HttpMethod.POST,
            label: 'Move',
            title: 'Move the change',
            enabled: true,
          },
        };
        await element.updateComplete;
      });

      test('works', () => {
        element.handleMoveTap();

        element.handleMoveConfirm();
        assert.equal(fireActionStub.callCount, 0);

        queryAndAssert<GrConfirmMoveDialog>(element, '#confirmMove').branch =
          'master' as BranchName;
        element.handleMoveConfirm();
        assert.equal(fireActionStub.callCount, 1);
      });

      test('branch name cleared when re-open move', () => {
        const emptyBranchName = '';
        queryAndAssert<GrConfirmMoveDialog>(element, '#confirmMove').branch =
          'master' as BranchName;

        element.handleMoveTap();
        assert.equal(
          queryAndAssert<GrConfirmMoveDialog>(element, '#confirmMove').branch,
          emptyBranchName
        );
      });
    });

    test('custom actions', async () => {
      // Add a button with the same key as a server-based one to ensure
      // collisions are taken care of.
      const key = element.addActionButton(element.ActionType.REVISION, 'Bork!');
      const keyTapped = mockPromise();
      element.addEventListener(key + '-tap', async e => {
        assert.equal(
          (e as CustomEvent).detail.node.getAttribute('data-action-key'),
          key
        );
        element.removeActionButton(key);
        await element.updateComplete;
        assert.notOk(query(element, '[data-action-key="' + key + '"]'));
        keyTapped.resolve();
      });
      await element.updateComplete;
      await element.updateComplete;
      queryAndAssert<GrButton>(
        element,
        '[data-action-key="' + key + '"]'
      ).click();
      await keyTapped;
    });

    test('setLoadingOnButtonWithKey top-level', () => {
      const key = 'rebase';
      const type = 'revision';
      const cleanup = element.setLoadingOnButtonWithKey(type, key);
      assert.equal(element.actionLoadingMessage, 'Rebasing...');

      const button = queryAndAssert<GrButton>(
        element,
        '[data-action-key="' + key + '"]'
      );
      assert.isTrue(button.hasAttribute('loading'));
      assert.isTrue(button.disabled);

      assert.isOk(cleanup);
      assert.isFunction(cleanup);
      cleanup();

      assert.isFalse(button.hasAttribute('loading'));
      assert.isFalse(button.disabled);
      assert.isNotOk(element.actionLoadingMessage);
    });

    test('setLoadingOnButtonWithKey overflow menu', () => {
      const key = 'cherrypick';
      const type = 'revision';
      const cleanup = element.setLoadingOnButtonWithKey(type, key);
      assert.equal(element.actionLoadingMessage, 'Cherry-picking...');
      assert.include(element.disabledMenuActions, 'cherrypick');
      assert.isFunction(cleanup);

      cleanup();

      assert.notOk(element.actionLoadingMessage);
      assert.notInclude(element.disabledMenuActions, 'cherrypick');
    });

    suite('abandon change', () => {
      let alertStub: sinon.SinonStub;
      let fireActionStub: sinon.SinonStub;

      setup(async () => {
        fireActionStub = sinon.stub(element, 'fireAction');
        alertStub = sinon.stub(window, 'alert');
        element.actions = {
          abandon: {
            method: HttpMethod.POST,
            label: 'Abandon',
            title: 'Abandon the change',
            enabled: true,
          },
        };
        await element.updateComplete;
        // test
        await element.reload();
      });

      test('abandon change with message', async () => {
        const newAbandonMsg = 'Test Abandon Message';
        queryAndAssert<GrConfirmAbandonDialog>(
          element,
          '#confirmAbandonDialog'
        ).message = newAbandonMsg;
        await element.updateComplete;
        queryAndAssert<GrButton>(
          element,
          'gr-button[data-action-key="abandon"]'
        ).click();

        assert.equal(
          queryAndAssert<GrConfirmAbandonDialog>(
            element,
            '#confirmAbandonDialog'
          ).message,
          newAbandonMsg
        );
      });

      test('abandon change with no message', async () => {
        await element.updateComplete;
        queryAndAssert<GrButton>(
          element,
          'gr-button[data-action-key="abandon"]'
        ).click();

        assert.equal(
          queryAndAssert<GrConfirmAbandonDialog>(
            element,
            '#confirmAbandonDialog'
          ).message,
          ''
        );
      });

      test('works', () => {
        queryAndAssert<GrConfirmAbandonDialog>(
          element,
          '#confirmAbandonDialog'
        ).message = 'original message';
        queryAndAssert<GrButton>(
          element,
          'gr-button[data-action-key="abandon"]'
        ).click();

        queryAndAssert<GrConfirmAbandonDialog>(
          element,
          '#confirmAbandonDialog'
        ).message = 'foo message';
        element.handleAbandonDialogConfirm();
        assert.notOk(alertStub.called);

        const action = {
          __key: 'abandon',
          __type: 'change',
          __primary: false,
          enabled: true,
          label: 'Abandon',
          method: HttpMethod.POST,
          title: 'Abandon the change',
        };
        assert.deepEqual(fireActionStub.lastCall.args, [
          '/abandon',
          action,
          false,
          {
            message: 'foo message',
          },
        ]);
      });
    });

    suite('revert change', () => {
      let fireActionStub: sinon.SinonStub;

      setup(async () => {
        fireActionStub = sinon.stub(element, 'fireAction');
        element.commitMessage = 'random commit message';
        element.change = {
          ...createChangeViewChange(),
          current_revision: 'abcdef' as CommitId,
          actions: {
            revert: {
              method: HttpMethod.POST,
              label: 'Revert',
              title: 'Revert the change',
              enabled: true,
            },
          },
        };
        await element.updateComplete;
        // test
        await element.reload();
      });

      test('revert change with plugin hook', async () => {
        const newRevertMsg = 'Modified revert msg';
        sinon
          .stub(
            queryAndAssert<GrConfirmRevertDialog>(
              element,
              '#confirmRevertDialog'
            ),
            'modifyRevertMsg'
          )
          .callsFake(() => newRevertMsg);
        element.change = {<commit_msg>Add test for revert action payload<commit_after>/**
 * @license
 * Copyright 2016 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import '../../../test/common-test-setup';
import './gr-change-actions';
import {navigationToken} from '../../core/gr-navigation/gr-navigation';
import {
  createAccountWithId,
  createApproval,
  createChange,
  createChangeMessages,
  createChangeViewChange,
  createRevision,
  createRevisions,
} from '../../../test/test-data-generators';
import {ChangeStatus, HttpMethod} from '../../../constants/constants';
import {
  mockPromise,
  query,
  queryAll,
  queryAndAssert,
  stubReporting,
  stubRestApi,
} from '../../../test/test-utils';
import {assertUIActionInfo, GrChangeActions} from './gr-change-actions';
import {
  AccountId,
  ActionInfo,
  ActionNameToActionInfoMap,
  BranchName,
  ChangeId,
  ChangeSubmissionId,
  CommitId,
  NumericChangeId,
  PatchSetNum,
  PatchSetNumber,
  RepoName,
  ReviewInput,
  TopicName,
} from '../../../types/common';
import {ActionType} from '../../../api/change-actions';
import {SinonFakeTimers} from 'sinon';
import {IronAutogrowTextareaElement} from '@polymer/iron-autogrow-textarea';
import {GrButton} from '../../shared/gr-button/gr-button';
import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
import {UIActionInfo} from '../../shared/gr-js-api-interface/gr-change-actions-js-api';
import {fixture, html, assert} from '@open-wc/testing';
import {GrConfirmCherrypickDialog} from '../gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog';
import {GrDropdown} from '../../shared/gr-dropdown/gr-dropdown';
import {GrConfirmSubmitDialog} from '../gr-confirm-submit-dialog/gr-confirm-submit-dialog';
import {GrConfirmRebaseDialog} from '../gr-confirm-rebase-dialog/gr-confirm-rebase-dialog';
import {GrConfirmMoveDialog} from '../gr-confirm-move-dialog/gr-confirm-move-dialog';
import {GrConfirmAbandonDialog} from '../gr-confirm-abandon-dialog/gr-confirm-abandon-dialog';
import {GrConfirmRevertDialog} from '../gr-confirm-revert-dialog/gr-confirm-revert-dialog';
import {EventType} from '../../../types/events';
import {testResolver} from '../../../test/common-test-setup';
import {storageServiceToken} from '../../../services/storage/gr-storage_impl';
import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';

// TODO(dhruvsri): remove use of _populateRevertMessage as it's private
suite('gr-change-actions tests', () => {
  let element: GrChangeActions;

  suite('basic tests', () => {
    setup(async () => {
      stubRestApi('getChangeRevisionActions').returns(
        Promise.resolve({
          cherrypick: {
            method: HttpMethod.POST,
            label: 'Cherry Pick',
            title: 'Cherry pick change to a different branch',
            enabled: true,
          },
          rebase: {
            method: HttpMethod.POST,
            label: 'Rebase',
            title: 'Rebase onto tip of branch or parent change',
            enabled: true,
          },
          submit: {
            method: HttpMethod.POST,
            label: 'Submit',
            title: 'Submit patch set 2 into master',
            enabled: true,
          },
          revert_submission: {
            method: HttpMethod.POST,
            label: 'Revert submission',
            title: 'Revert this submission',
            enabled: true,
          },
        })
      );
      stubRestApi('send').callsFake((method, url) => {
        if (method !== 'POST') {
          return Promise.reject(new Error('bad method'));
        }
        if (url === '/changes/test~42/revisions/2/submit') {
          return Promise.resolve({
            ...new Response(),
            ok: true,
            text() {
              return Promise.resolve(")]}'\n{}");
            },
          });
        } else if (url === '/changes/test~42/revisions/2/rebase') {
          return Promise.resolve({
            ...new Response(),
            ok: true,
            text() {
              return Promise.resolve(")]}'\n{}");
            },
          });
        }
        return Promise.reject(new Error('bad url'));
      });

      sinon
        .stub(testResolver(pluginLoaderToken), 'awaitPluginsLoaded')
        .returns(Promise.resolve());

      element = await fixture<GrChangeActions>(html`
        <gr-change-actions></gr-change-actions>
      `);
      element.change = {
        ...createChangeViewChange(),
        actions: {
          '/': {
            method: HttpMethod.DELETE,
            label: 'Delete Change',
            title: 'Delete change X_X',
            enabled: true,
          },
        },
      };
      element.changeNum = 42 as NumericChangeId;
      element.latestPatchNum = 2 as PatchSetNum;
      element.account = {
        _account_id: 123 as AccountId,
      };
      stubRestApi('getRepoBranches').returns(Promise.resolve([]));

      await element.updateComplete;
      await element.reload();
    });

    test('render', () => {
      assert.shadowDom.equal(
        element,
        /* HTML */ `
          <div id="mainContent">
            <span hidden="" id="actionLoadingMessage"> </span>
            <section id="primaryActions">
              <gr-tooltip-content
                has-tooltip=""
                position-below=""
                title="Submit patch set 2 into master"
              >
                <gr-button
                  aria-disabled="false"
                  class="submit"
                  data-action-key="submit"
                  data-label="Submit"
                  link=""
                  role="button"
                  tabindex="0"
                >
                  <gr-icon icon="done_all"></gr-icon>
                  Submit
                </gr-button>
              </gr-tooltip-content>
            </section>
            <section id="secondaryActions">
              <gr-tooltip-content
                has-tooltip=""
                position-below=""
                title="Rebase onto tip of branch or parent change"
              >
                <gr-button
                  aria-disabled="true"
                  class="rebase"
                  data-action-key="rebase"
                  data-label="Rebase"
                  disabled=""
                  link=""
                  role="button"
                  tabindex="-1"
                >
                  <gr-icon icon="rebase"> </gr-icon>
                  Rebase
                </gr-button>
              </gr-tooltip-content>
            </section>
            <gr-button
              aria-disabled="false"
              hidden=""
              role="button"
              tabindex="0"
            >
              Loading actions...
            </gr-button>
            <gr-dropdown id="moreActions" link="">
              <gr-icon icon="more_vert" aria-labelledby="moreMessage"></gr-icon>
              <span id="moreMessage"> More </span>
            </gr-dropdown>
          </div>
          <dialog id="actionsModal" tabindex="-1">
            <gr-confirm-rebase-dialog class="confirmDialog" id="confirmRebase">
            </gr-confirm-rebase-dialog>
            <gr-confirm-cherrypick-dialog
              class="confirmDialog"
              id="confirmCherrypick"
            >
            </gr-confirm-cherrypick-dialog>
            <gr-confirm-cherrypick-conflict-dialog
              class="confirmDialog"
              id="confirmCherrypickConflict"
            >
            </gr-confirm-cherrypick-conflict-dialog>
            <gr-confirm-move-dialog class="confirmDialog" id="confirmMove">
            </gr-confirm-move-dialog>
            <gr-confirm-revert-dialog
              class="confirmDialog"
              id="confirmRevertDialog"
            >
            </gr-confirm-revert-dialog>
            <gr-confirm-abandon-dialog
              class="confirmDialog"
              id="confirmAbandonDialog"
            >
            </gr-confirm-abandon-dialog>
            <gr-confirm-submit-dialog
              class="confirmDialog"
              id="confirmSubmitDialog"
            >
            </gr-confirm-submit-dialog>
            <gr-dialog
              class="confirmDialog"
              confirm-label="Create"
              id="createFollowUpDialog"
              role="dialog"
            >
              <div class="header" slot="header">Create Follow-Up Change</div>
              <div class="main" slot="main">
                <gr-create-change-dialog id="createFollowUpChange">
                </gr-create-change-dialog>
              </div>
            </gr-dialog>
            <gr-dialog
              class="confirmDialog"
              confirm-label="Delete"
              confirm-on-enter=""
              id="confirmDeleteDialog"
              role="dialog"
            >
              <div class="header" slot="header">Delete Change</div>
              <div class="main" slot="main">
                Do you really want to delete the change?
              </div>
            </gr-dialog>
            <gr-dialog
              class="confirmDialog"
              confirm-label="Delete"
              confirm-on-enter=""
              id="confirmDeleteEditDialog"
              role="dialog"
            >
              <div class="header" slot="header">Delete Change Edit</div>
              <div class="main" slot="main">
                Do you really want to delete the edit?
              </div>
            </gr-dialog>
          </dialog>
        `
      );
    });

    test('show-revision-actions event should fire', async () => {
      const spy = sinon.spy(element, 'sendShowRevisionActions');
      element.reload();
      await element.updateComplete;
      assert.isTrue(spy.called);
    });

    test('primary and secondary actions split properly', () => {
      // Submit should be the only primary action.
      assert.equal(element.topLevelPrimaryActions!.length, 1);
      assert.equal(element.topLevelPrimaryActions![0].label, 'Submit');
      assert.equal(
        element.topLevelSecondaryActions!.length,
        element.topLevelActions!.length - 1
      );
    });

    test('revert submission action is skipped', () => {
      assert.equal(
        element.allActionValues.filter(action => action.__key === 'submit')
          .length,
        1
      );
      assert.equal(
        element.allActionValues.filter(
          action => action.__key === 'revert_submission'
        ).length,
        0
      );
    });

    test('plugin revision actions', async () => {
      const stub = stubRestApi('getChangeActionURL').returns(
        Promise.resolve('the-url')
      );
      element.revisionActions = {
        'plugin~action': {},
      };
      assert.isOk(element.revisionActions['plugin~action']);
      await element.updateComplete;
      assert.isTrue(
        stub.calledWith(
          element.changeNum,
          element.latestPatchNum,
          '/plugin~action'
        )
      );
      assert.equal(
        (element.revisionActions['plugin~action'] as UIActionInfo)!.__url,
        'the-url'
      );
    });

    test('plugin change actions', async () => {
      const stub = stubRestApi('getChangeActionURL').returns(
        Promise.resolve('the-url')
      );
      element.actions = {
        'plugin~action': {},
      };
      assert.isOk(element.actions['plugin~action']);
      await element.updateComplete;
      assert.isTrue(
        stub.calledWith(element.changeNum, undefined, '/plugin~action')
      );
      assert.equal(
        (element.actions['plugin~action'] as UIActionInfo)!.__url,
        'the-url'
      );
    });

    test('not supported actions are filtered out', () => {
      element.revisionActions = {followup: {}};
      assert.equal(
        element.querySelectorAll(
          'section gr-button[data-action-type="revision"]'
        ).length,
        0
      );
    });

    test('getActionDetails', () => {
      element.revisionActions = {
        'plugin~action': {},
        ...element.revisionActions,
      };
      assert.isUndefined(element.getActionDetails('rubbish'));
      assert.strictEqual(
        element.revisionActions['plugin~action'],
        element.getActionDetails('plugin~action')
      );
      assert.strictEqual(
        element.revisionActions['rebase'],
        element.getActionDetails('rebase')
      );
    });

    test('hide revision action', async () => {
      await element.updateComplete;
      let buttonEl: Element | undefined = queryAndAssert(
        element,
        '[data-action-key="submit"]'
      );
      assert.isOk(buttonEl);
      element.setActionHidden(
        element.ActionType.REVISION,
        element.RevisionActions.SUBMIT,
        true
      );
      assert.lengthOf(element.hiddenActions, 1);
      await element.updateComplete;
      buttonEl = query(element, '[data-action-key="submit"]');
      assert.isNotOk(buttonEl);

      element.setActionHidden(
        element.ActionType.REVISION,
        element.RevisionActions.SUBMIT,
        false
      );
      await element.updateComplete;
      buttonEl = queryAndAssert(element, '[data-action-key="submit"]');
      assert.isFalse(buttonEl.hasAttribute('hidden'));
    });

    test('buttons exist', async () => {
      element.loading = false;
      await element.updateComplete;
      const buttonEls = queryAll(element, 'gr-button');
      const menuItems = queryAndAssert<GrDropdown>(
        element,
        '#moreActions'
      ).items;

      // Total button number is one greater than the number of total actions
      // due to the existence of the overflow menu trigger.
      assert.equal(
        buttonEls.length + menuItems!.length,
        element.allActionValues.length + 1
      );
      assert.isFalse(element.hidden);
    });

    test('delete buttons have explicit labels', async () => {
      await element.updateComplete;
      const deleteItems = queryAndAssert<GrDropdown>(
        element,
        '#moreActions'
      ).items!.filter(item => item.id!.startsWith('delete'));
      assert.equal(deleteItems.length, 1);
      assert.equal(deleteItems[0].name, 'Delete change');
    });

    test('get revision object from change', () => {
      const revObj = {
        ...createRevision(2),
        foo: 'bar',
      };
      const change = {
        ...createChangeViewChange(),
        revisions: {
          rev1: createRevision(1),
          rev2: revObj,
        },
      };
      assert.deepEqual(
        element.getRevision(change, 2 as PatchSetNumber),
        revObj
      );
    });

    test('actionComparator sort order', () => {
      const actions = [
        {label: '123', __type: ActionType.CHANGE, __key: 'review'},
        {label: 'abc-ro', __type: ActionType.REVISION, __key: 'random'},
        {label: 'abc', __type: ActionType.CHANGE, __key: 'random'},
        {label: 'def', __type: ActionType.CHANGE, __key: 'random'},
        {
          label: 'def-p',
          __type: ActionType.CHANGE,
          __primary: true,
          __key: 'random',
        },
      ];

      const result = actions.slice();
      result.reverse();
      result.sort(element.actionComparator.bind(element));
      assert.deepEqual(result, actions);
    });

    test('submit change', async () => {
      const showSpy = sinon.spy(element, 'showActionDialog');
      stubRestApi('getFromProjectLookup').returns(
        Promise.resolve('test' as RepoName)
      );
      element.change = {
        ...createChangeViewChange(),
        revisions: {
          rev1: {...createRevision(), _number: 1 as PatchSetNumber},
          rev2: {...createRevision(), _number: 2 as PatchSetNumber},
        },
      };
      element.latestPatchNum = 2 as PatchSetNumber;

      queryAndAssert<GrButton>(
        element,
        'gr-button[data-action-key="submit"]'
      ).click();

      await element.updateComplete;
      assert.isTrue(
        showSpy.calledWith(
          queryAndAssert<GrConfirmSubmitDialog>(element, '#confirmSubmitDialog')
        )
      );
    });

    test('submit change, tap on icon', async () => {
      const submitted = mockPromise();
      sinon
        .stub(
          queryAndAssert<GrConfirmSubmitDialog>(
            element,
            '#confirmSubmitDialog'
          ),
          'resetFocus'
        )
        .callsFake(() => submitted.resolve());
      stubRestApi('getFromProjectLookup').returns(
        Promise.resolve('test' as RepoName)
      );
      element.change = {
        ...createChangeViewChange(),
        revisions: {
          rev1: {...createRevision(), _number: 1 as PatchSetNumber},
          rev2: {...createRevision(), _number: 2 as PatchSetNumber},
        },
      };
      element.latestPatchNum = 2 as PatchSetNum;

      queryAndAssert<GrButton>(
        element,
        'gr-button[data-action-key="submit"] gr-icon'
      ).click();
      await submitted;
    });

    test('correct icons', async () => {
      element.loggedIn = true;
      await element.updateComplete;

      queryAndAssert<GrButton>(
        element,
        'gr-button[data-action-key="submit"] gr-icon'
      );
      queryAndAssert<GrButton>(
        element,
        'gr-button[data-action-key="rebase"] gr-icon'
      );
      queryAndAssert<GrButton>(
        element,
        'gr-button[data-action-key="edit"] gr-icon[filled]'
      );
    });

    test('handleSubmitConfirm', () => {
      const fireStub = sinon.stub(element, 'fireAction');
      sinon.stub(element, 'canSubmitChange').returns(true);
      element.handleSubmitConfirm();
      assert.isTrue(fireStub.calledOnce);
      assert.deepEqual(fireStub.lastCall.args, [
        '/submit',
        assertUIActionInfo(element.revisionActions.submit),
        true,
      ]);
    });

    test('handleSubmitConfirm when not able to submit', () => {
      const fireStub = sinon.stub(element, 'fireAction');
      sinon.stub(element, 'canSubmitChange').returns(false);
      element.handleSubmitConfirm();
      assert.isFalse(fireStub.called);
    });

    test('submit change with plugin hook', async () => {
      sinon.stub(element, 'canSubmitChange').callsFake(() => false);
      const fireActionStub = sinon.stub(element, 'fireAction');
      await element.updateComplete;
      queryAndAssert<GrButton>(
        element,
        'gr-button[data-action-key="submit"]'
      ).click();
      assert.equal(fireActionStub.callCount, 0);
    });

    test('chain state', async () => {
      assert.equal(element._hasKnownChainState, false);
      element.hasParent = true;
      await element.updateComplete;
      assert.equal(element._hasKnownChainState, true);
    });

    test('calculateDisabled', () => {
      const action = {
        __key: 'rebase',
        enabled: true,
        __type: ActionType.CHANGE,
        label: 'l',
      };
      element._hasKnownChainState = false;
      assert.equal(element.calculateDisabled(action), true);

      action.__key = 'delete';
      assert.equal(element.calculateDisabled(action), false);

      action.__key = 'rebase';
      element._hasKnownChainState = true;
      assert.equal(element.calculateDisabled(action), false);

      action.enabled = false;
      assert.equal(element.calculateDisabled(action), false);
    });

    test('rebase change', async () => {
      const fireActionStub = sinon.stub(element, 'fireAction');
      const fetchChangesStub = sinon
        .stub(
          queryAndAssert<GrConfirmRebaseDialog>(element, '#confirmRebase'),
          'fetchRecentChanges'
        )
        .returns(Promise.resolve([]));
      element._hasKnownChainState = true;
      await element.updateComplete;
      queryAndAssert<GrButton>(
        element,
        'gr-button[data-action-key="rebase"]'
      ).click();
      const rebaseAction = {
        __key: 'rebase',
        __type: 'revision',
        __primary: false,
        enabled: true,
        label: 'Rebase',
        method: HttpMethod.POST,
        title: 'Rebase onto tip of branch or parent change',
      };
      assert.isTrue(fetchChangesStub.called);
      element.handleRebaseConfirm(
        new CustomEvent('', {detail: {base: '1234', allowConflicts: false}})
      );
      assert.deepEqual(fireActionStub.lastCall.args, [
        '/rebase',
        assertUIActionInfo(rebaseAction),
        true,
        {base: '1234', allow_conflicts: false},
        {allow_conflicts: false},
      ]);
    });

    test('rebase change fires reload event', async () => {
      const eventStub = sinon.stub(element, 'dispatchEvent');
      await element.handleResponse(
        {__key: 'rebase', __type: ActionType.CHANGE, label: 'l'},
        new Response()
      );
      assert.isTrue(eventStub.called);
      assert.equal(eventStub.lastCall.args[0].type, 'reload');
    });

    test("rebase dialog gets recent changes each time it's opened", async () => {
      const fetchChangesStub = sinon
        .stub(
          queryAndAssert<GrConfirmRebaseDialog>(element, '#confirmRebase'),
          'fetchRecentChanges'
        )
        .returns(Promise.resolve([]));
      element._hasKnownChainState = true;
      await element.updateComplete;
      const rebaseButton = queryAndAssert<GrButton>(
        element,
        'gr-button[data-action-key="rebase"]'
      );
      rebaseButton.click();
      await element.updateComplete;
      assert.isTrue(fetchChangesStub.calledOnce);

      await element.updateComplete;
      queryAndAssert<GrConfirmRebaseDialog>(
        element,
        '#confirmRebase'
      ).dispatchEvent(
        new CustomEvent('cancel', {
          composed: true,
          bubbles: true,
        })
      );
      rebaseButton.click();
      assert.isTrue(fetchChangesStub.calledTwice);
    });

    test('two dialogs are not shown at the same time', async () => {
      element._hasKnownChainState = true;
      await element.updateComplete;
      queryAndAssert<GrButton>(
        element,
        'gr-button[data-action-key="rebase"]'
      ).click();
      await element.updateComplete;
      assert.isFalse(
        queryAndAssert<GrConfirmRebaseDialog>(element, '#confirmRebase').hidden
      );
      stubRestApi('getChanges').returns(Promise.resolve([]));
      element.handleCherrypickTap();
      await element.updateComplete;
      assert.isTrue(
        queryAndAssert<GrConfirmRebaseDialog>(element, '#confirmRebase').hidden
      );
      assert.isFalse(
        queryAndAssert<GrConfirmCherrypickDialog>(element, '#confirmCherrypick')
          .hidden
      );
    });

    test('setReviewOnRevert', () => {
      const review = {labels: {Foo: 1, 'Bar-Baz': -2}};
      const changeId = 1234 as NumericChangeId;
      sinon
        .stub(
          testResolver(pluginLoaderToken).jsApiService,
          'getReviewPostRevert'
        )
        .returns(review);
      const saveStub = stubRestApi('saveChangeReview').returns(
        Promise.resolve(new Response())
      );
      const setReviewOnRevert = element.setReviewOnRevert(changeId) as Promise<
        undefined | Response
      >;
      return setReviewOnRevert.then((_res: Response | undefined) => {
        assert.isTrue(saveStub.calledOnce);
        assert.equal(saveStub.lastCall.args[0], changeId);
        assert.deepEqual(saveStub.lastCall.args[2], review);
      });
    });

    suite('change edits', () => {
      test('disableEdit', async () => {
        element.editMode = false;
        element.editBasedOnCurrentPatchSet = false;
        element.change = {
          ...createChangeViewChange(),
          status: ChangeStatus.NEW,
        };
        element.disableEdit = true;
        await element.updateComplete;

        assert.isNotOk(
          query(element, 'gr-button[data-action-key="publishEdit"]')
        );
        assert.isNotOk(
          query(element, 'gr-button[data-action-key="rebaseEdit"]')
        );
        assert.isNotOk(
          query(element, 'gr-button[data-action-key="deleteEdit"]')
        );
        assert.isNotOk(query(element, 'gr-button[data-action-key="edit"]'));
        assert.isNotOk(query(element, 'gr-button[data-action-key="stopEdit"]'));
      });

      test('shows confirm dialog for delete edit', async () => {
        element.loggedIn = true;
        element.editMode = true;
        element.editPatchsetLoaded = true;
        await element.updateComplete;

        const fireActionStub = sinon.stub(element, 'fireAction');
        element.handleDeleteEditTap();
        assert.isFalse(
          queryAndAssert<GrDialog>(element, '#confirmDeleteEditDialog').hidden
        );
        queryAndAssert<GrButton>(
          queryAndAssert(element, '#confirmDeleteEditDialog'),
          'gr-button[primary]'
        ).click();
        await element.updateComplete;

        assert.equal(fireActionStub.lastCall.args[0], '/edit');
      });

      test('all cached change edits get deleted on delete edit', async () => {
        element.loggedIn = true;
        element.editMode = true;
        element.editPatchsetLoaded = true;
        await element.updateComplete;

        const storage = testResolver(storageServiceToken);
        storage.setEditableContentItem(
          'c42_ps2_index.php',
          '<?php\necho 42_ps_2'
        );
        storage.setEditableContentItem(
          'c42_psedit_index.php',
          '<?php\necho 42_ps_edit'
        );

        assert.equal(
          storage.getEditableContentItem('c42_ps2_index.php')!.message,
          '<?php\necho 42_ps_2'
        );
        assert.equal(
          storage.getEditableContentItem('c42_psedit_index.php')!.message,
          '<?php\necho 42_ps_edit'
        );

        assert.isOk(storage.getEditableContentItem('c42_psedit_index.php')!);
        assert.isOk(storage.getEditableContentItem('c42_ps2_index.php')!);
        assert.isNotOk(storage.getEditableContentItem('c50_psedit_index.php')!);

        const eraseEditableContentItemsForChangeEditSpy = sinon.spy(
          storage,
          'eraseEditableContentItemsForChangeEdit'
        );
        sinon.stub(element, 'fireAction');
        element.handleDeleteEditTap();
        assert.isFalse(
          queryAndAssert<GrDialog>(element, '#confirmDeleteEditDialog').hidden
        );
        queryAndAssert<GrButton>(
          queryAndAssert(element, '#confirmDeleteEditDialog'),
          'gr-button[primary]'
        ).click();
        await element.updateComplete;
        assert.isTrue(eraseEditableContentItemsForChangeEditSpy.called);
        assert.isNotOk(storage.getEditableContentItem('c42_psedit_index.php')!);
        assert.isNotOk(storage.getEditableContentItem('c42_ps2_index.php')!);
      });

      test('edit patchset is loaded, needs rebase', async () => {
        element.loggedIn = true;
        element.editMode = true;
        element.editPatchsetLoaded = true;
        element.change = {
          ...createChangeViewChange(),
          status: ChangeStatus.NEW,
        };
        element.editBasedOnCurrentPatchSet = false;
        await element.updateComplete;

        assert.isNotOk(
          query(element, 'gr-button[data-action-key="publishEdit"]')
        );
        assert.isOk(query(element, 'gr-button[data-action-key="rebaseEdit"]'));
        assert.isOk(query(element, 'gr-button[data-action-key="deleteEdit"]'));
        assert.isNotOk(query(element, 'gr-button[data-action-key="edit"]'));
        assert.isNotOk(query(element, 'gr-button[data-action-key="stopEdit"]'));
      });

      test('edit patchset is loaded, does not need rebase', async () => {
        element.loggedIn = true;
        element.editMode = true;
        element.editPatchsetLoaded = true;
        element.change = {
          ...createChangeViewChange(),
          status: ChangeStatus.NEW,
        };
        element.editBasedOnCurrentPatchSet = true;
        await element.updateComplete;

        assert.isOk(query(element, 'gr-button[data-action-key="publishEdit"]'));
        assert.isNotOk(
          query(element, 'gr-button[data-action-key="rebaseEdit"]')
        );
        assert.isOk(query(element, 'gr-button[data-action-key="deleteEdit"]'));
        assert.isNotOk(query(element, 'gr-button[data-action-key="edit"]'));
        assert.isNotOk(query(element, 'gr-button[data-action-key="stopEdit"]'));
      });

      test('edit mode is loaded, no edit patchset', async () => {
        element.loggedIn = true;
        element.editMode = true;
        element.editPatchsetLoaded = false;
        element.change = {
          ...createChangeViewChange(),
          status: ChangeStatus.NEW,
        };
        await element.updateComplete;

        assert.isNotOk(
          query(element, 'gr-button[data-action-key="publishEdit"]')
        );
        assert.isNotOk(
          query(element, 'gr-button[data-action-key="rebaseEdit"]')
        );
        assert.isNotOk(
          query(element, 'gr-button[data-action-key="deleteEdit"]')
        );
        assert.isNotOk(query(element, 'gr-button[data-action-key="edit"]'));
        assert.isOk(query(element, 'gr-button[data-action-key="stopEdit"]'));
      });

      test('normal patch set', async () => {
        element.loggedIn = true;
        element.editMode = false;
        element.editPatchsetLoaded = false;
        element.change = {
          ...createChangeViewChange(),
          status: ChangeStatus.NEW,
        };
        await element.updateComplete;

        assert.isNotOk(
          query(element, 'gr-button[data-action-key="publishEdit"]')
        );
        assert.isNotOk(
          query(element, 'gr-button[data-action-key="rebaseEdit"]')
        );
        assert.isNotOk(
          query(element, 'gr-button[data-action-key="deleteEdit"]')
        );
        assert.isOk(query(element, 'gr-button[data-action-key="edit"]'));
        assert.isNotOk(query(element, 'gr-button[data-action-key="stopEdit"]'));
      });

      test('edit action', async () => {
        element.loggedIn = true;
        const editTapped = mockPromise();
        element.addEventListener('edit-tap', () => {
          editTapped.resolve();
        });
        element.editMode = true;
        element.change = {
          ...createChangeViewChange(),
          status: ChangeStatus.NEW,
        };
        await element.updateComplete;

        assert.isNotOk(query(element, 'gr-button[data-action-key="edit"]'));
        assert.isOk(query(element, 'gr-button[data-action-key="stopEdit"]'));
        element.change = {
          ...createChangeViewChange(),
          status: ChangeStatus.MERGED,
        };
        await element.updateComplete;

        assert.isNotOk(query(element, 'gr-button[data-action-key="edit"]'));
        element.change = {
          ...createChangeViewChange(),
          status: ChangeStatus.NEW,
        };
        element.editMode = false;
        await element.updateComplete;

        queryAndAssert<GrButton>(
          element,
          'gr-button[data-action-key="edit"]'
        ).click();
        await editTapped;
      });
    });

    test('edit action not shown for logged out user', async () => {
      element.loggedIn = false;
      element.editMode = false;
      element.editPatchsetLoaded = false;
      element.change = {
        ...createChangeViewChange(),
        status: ChangeStatus.NEW,
      };
      await element.updateComplete;

      assert.isNotOk(
        query(element, 'gr-button[data-action-key="publishEdit"]')
      );
      assert.isNotOk(query(element, 'gr-button[data-action-key="rebaseEdit"]'));
      assert.isNotOk(query(element, 'gr-button[data-action-key="deleteEdit"]'));
      assert.isNotOk(query(element, 'gr-button[data-action-key="edit"]'));
      assert.isNotOk(query(element, 'gr-button[data-action-key="stopEdit"]'));
    });

    suite('cherry-pick', () => {
      let fireActionStub: sinon.SinonStub;

      setup(() => {
        fireActionStub = sinon.stub(element, 'fireAction');
        sinon.stub(window, 'alert');
      });

      test('works', async () => {
        element.handleCherrypickTap();
        const action = {
          __key: 'cherrypick',
          __type: 'revision',
          __primary: false,
          enabled: true,
          label: 'Cherry pick',
          method: HttpMethod.POST,
          title: 'Cherry pick change to a different branch',
        };

        element.handleCherrypickConfirm();
        assert.equal(fireActionStub.callCount, 0);

        queryAndAssert<GrConfirmCherrypickDialog>(
          element,
          '#confirmCherrypick'
        ).branch = 'master' as BranchName;
        element.handleCherrypickConfirm();
        assert.equal(fireActionStub.callCount, 0); // Still needs a message.

        // Add attributes that are used to determine the message.
        queryAndAssert<GrConfirmCherrypickDialog>(
          element,
          '#confirmCherrypick'
        ).commitMessage = 'foo message';
        queryAndAssert<GrConfirmCherrypickDialog>(
          element,
          '#confirmCherrypick'
        ).changeStatus = ChangeStatus.NEW;
        queryAndAssert<GrConfirmCherrypickDialog>(
          element,
          '#confirmCherrypick'
        ).commitNum = '123' as CommitId;
        await element.updateComplete;

        element.handleCherrypickConfirm();
        await element.updateComplete;

        const autogrowEl = queryAndAssert<IronAutogrowTextareaElement>(
          queryAndAssert<GrConfirmCherrypickDialog>(
            element,
            '#confirmCherrypick'
          ),
          '#messageInput'
        );
        assert.equal(autogrowEl.value, 'foo message');

        assert.deepEqual(fireActionStub.lastCall.args, [
          '/cherrypick',
          action,
          true,
          {
            destination: 'master',
            base: null,
            message: 'foo message',
            allow_conflicts: false,
          },
        ]);
      });

      test('cherry pick even with conflicts', async () => {
        element.handleCherrypickTap();
        const action = {
          __key: 'cherrypick',
          __type: 'revision',
          __primary: false,
          enabled: true,
          label: 'Cherry pick',
          method: HttpMethod.POST,
          title: 'Cherry pick change to a different branch',
        };

        queryAndAssert<GrConfirmCherrypickDialog>(
          element,
          '#confirmCherrypick'
        ).branch = 'master' as BranchName;

        // Add attributes that are used to determine the message.
        queryAndAssert<GrConfirmCherrypickDialog>(
          element,
          '#confirmCherrypick'
        ).commitMessage = 'foo message';
        queryAndAssert<GrConfirmCherrypickDialog>(
          element,
          '#confirmCherrypick'
        ).changeStatus = ChangeStatus.NEW;
        queryAndAssert<GrConfirmCherrypickDialog>(
          element,
          '#confirmCherrypick'
        ).commitNum = '123' as CommitId;
        await element.updateComplete;

        element.handleCherrypickConflictConfirm();
        await element.updateComplete;

        assert.deepEqual(fireActionStub.lastCall.args, [
          '/cherrypick',
          action,
          true,
          {
            destination: 'master',
            base: null,
            message: 'foo message',
            allow_conflicts: true,
          },
        ]);
      });

      test('branch name cleared when re-open cherrypick', () => {
        const emptyBranchName = '';
        queryAndAssert<GrConfirmCherrypickDialog>(
          element,
          '#confirmCherrypick'
        ).branch = 'master' as BranchName;

        element.handleCherrypickTap();
        assert.equal(
          queryAndAssert<GrConfirmCherrypickDialog>(
            element,
            '#confirmCherrypick'
          ).branch,
          emptyBranchName
        );
      });

      suite('cherry pick topics', () => {
        const changes = [
          {
            ...createChangeViewChange(),
            change_id: '12345678901234' as ChangeId,
            topic: 'T' as TopicName,
            subject: 'random',
            project: 'A' as RepoName,
            status: ChangeStatus.MERGED,
          },
          {
            ...createChangeViewChange(),
            change_id: '23456' as ChangeId,
            topic: 'T' as TopicName,
            subject: 'a'.repeat(100),
            project: 'B' as RepoName,
            status: ChangeStatus.NEW,
          },
        ];
        setup(async () => {
          stubRestApi('getChanges').returns(Promise.resolve(changes));
          element.handleCherrypickTap();
          await element.updateComplete;
          const confirmCherrypick = queryAndAssert<GrConfirmCherrypickDialog>(
            element,
            '#confirmCherrypick'
          );
          await element.updateComplete;
          const radioButtons = queryAll<HTMLInputElement>(
            confirmCherrypick,
            "input[name='cherryPickOptions']"
          );
          assert.equal(radioButtons.length, 2);
          radioButtons[1].click();
          await element.updateComplete;
        });

        test('cherry pick topic dialog is rendered', async () => {
          const dialog = queryAndAssert<GrConfirmCherrypickDialog>(
            element,
            '#confirmCherrypick'
          );
          await element.updateComplete;
          const changesTable = queryAndAssert(dialog, 'table');
          const headers = Array.from(changesTable.querySelectorAll('th'));
          const expectedHeadings = [
            '',
            'Change',
            'Status',
            'Subject',
            'Project',
            'Progress',
            '',
          ];
          const headings = headers.map(header => header.innerText);
          assert.equal(headings.length, expectedHeadings.length);
          for (let i = 0; i < headings.length; i++) {
            assert.equal(headings[i].trim(), expectedHeadings[i]);
          }
          const changeRows = queryAll(changesTable, 'tbody > tr');
          const change = Array.from(changeRows[0].querySelectorAll('td')).map(
            e => e.innerText
          );
          const expectedChange = [
            '',
            '1234567890',
            'MERGED',
            'random',
            'A',
            'NOT STARTED',
            '',
          ];
          for (let i = 0; i < change.length; i++) {
            assert.equal(change[i].trim(), expectedChange[i]);
          }
        });

        test('changes with duplicate project show an error', async () => {
          const dialog = queryAndAssert<GrConfirmCherrypickDialog>(
            element,
            '#confirmCherrypick'
          );
          const error = queryAndAssert<HTMLSpanElement>(
            dialog,
            '.error-message'
          );
          assert.equal(error.innerText, '');
          dialog.updateChanges([
            {
              ...createChangeViewChange(),
              change_id: '12345678901234' as ChangeId,
              topic: 'T' as TopicName,
              subject: 'random',
              project: 'A' as RepoName,
            },
            {
              ...createChangeViewChange(),
              change_id: '23456' as ChangeId,
              topic: 'T' as TopicName,
              subject: 'a'.repeat(100),
              project: 'A' as RepoName,
            },
          ]);
          await element.updateComplete;
          assert.equal(
            error.innerText,
            'Two changes cannot be of the same' + ' project'
          );
        });
      });
    });

    suite('move change', () => {
      let fireActionStub: sinon.SinonStub;

      setup(async () => {
        fireActionStub = sinon.stub(element, 'fireAction');
        sinon.stub(window, 'alert');
        element.actions = {
          move: {
            method: HttpMethod.POST,
            label: 'Move',
            title: 'Move the change',
            enabled: true,
          },
        };
        await element.updateComplete;
      });

      test('works', () => {
        element.handleMoveTap();

        element.handleMoveConfirm();
        assert.equal(fireActionStub.callCount, 0);

        queryAndAssert<GrConfirmMoveDialog>(element, '#confirmMove').branch =
          'master' as BranchName;
        element.handleMoveConfirm();
        assert.equal(fireActionStub.callCount, 1);
      });

      test('branch name cleared when re-open move', () => {
        const emptyBranchName = '';
        queryAndAssert<GrConfirmMoveDialog>(element, '#confirmMove').branch =
          'master' as BranchName;

        element.handleMoveTap();
        assert.equal(
          queryAndAssert<GrConfirmMoveDialog>(element, '#confirmMove').branch,
          emptyBranchName
        );
      });
    });

    test('custom actions', async () => {
      // Add a button with the same key as a server-based one to ensure
      // collisions are taken care of.
      const key = element.addActionButton(element.ActionType.REVISION, 'Bork!');
      const keyTapped = mockPromise();
      element.addEventListener(key + '-tap', async e => {
        assert.equal(
          (e as CustomEvent).detail.node.getAttribute('data-action-key'),
          key
        );
        element.removeActionButton(key);
        await element.updateComplete;
        assert.notOk(query(element, '[data-action-key="' + key + '"]'));
        keyTapped.resolve();
      });
      await element.updateComplete;
      await element.updateComplete;
      queryAndAssert<GrButton>(
        element,
        '[data-action-key="' + key + '"]'
      ).click();
      await keyTapped;
    });

    test('setLoadingOnButtonWithKey top-level', () => {
      const key = 'rebase';
      const type = 'revision';
      const cleanup = element.setLoadingOnButtonWithKey(type, key);
      assert.equal(element.actionLoadingMessage, 'Rebasing...');

      const button = queryAndAssert<GrButton>(
        element,
        '[data-action-key="' + key + '"]'
      );
      assert.isTrue(button.hasAttribute('loading'));
      assert.isTrue(button.disabled);

      assert.isOk(cleanup);
      assert.isFunction(cleanup);
      cleanup();

      assert.isFalse(button.hasAttribute('loading'));
      assert.isFalse(button.disabled);
      assert.isNotOk(element.actionLoadingMessage);
    });

    test('setLoadingOnButtonWithKey overflow menu', () => {
      const key = 'cherrypick';
      const type = 'revision';
      const cleanup = element.setLoadingOnButtonWithKey(type, key);
      assert.equal(element.actionLoadingMessage, 'Cherry-picking...');
      assert.include(element.disabledMenuActions, 'cherrypick');
      assert.isFunction(cleanup);

      cleanup();

      assert.notOk(element.actionLoadingMessage);
      assert.notInclude(element.disabledMenuActions, 'cherrypick');
    });

    suite('abandon change', () => {
      let alertStub: sinon.SinonStub;
      let fireActionStub: sinon.SinonStub;

      setup(async () => {
        fireActionStub = sinon.stub(element, 'fireAction');
        alertStub = sinon.stub(window, 'alert');
        element.actions = {
          abandon: {
            method: HttpMethod.POST,
            label: 'Abandon',
            title: 'Abandon the change',
            enabled: true,
          },
        };
        await element.updateComplete;
        // test
        await element.reload();
      });

      test('abandon change with message', async () => {
        const newAbandonMsg = 'Test Abandon Message';
        queryAndAssert<GrConfirmAbandonDialog>(
          element,
          '#confirmAbandonDialog'
        ).message = newAbandonMsg;
        await element.updateComplete;
        queryAndAssert<GrButton>(
          element,
          'gr-button[data-action-key="abandon"]'
        ).click();

        assert.equal(
          queryAndAssert<GrConfirmAbandonDialog>(
            element,
            '#confirmAbandonDialog'
          ).message,
          newAbandonMsg
        );
      });

      test('abandon change with no message', async () => {
        await element.updateComplete;
        queryAndAssert<GrButton>(
          element,
          'gr-button[data-action-key="abandon"]'
        ).click();

        assert.equal(
          queryAndAssert<GrConfirmAbandonDialog>(
            element,
            '#confirmAbandonDialog'
          ).message,
          ''
        );
      });

      test('works', () => {
        queryAndAssert<GrConfirmAbandonDialog>(
          element,
          '#confirmAbandonDialog'
        ).message = 'original message';
        queryAndAssert<GrButton>(
          element,
          'gr-button[data-action-key="abandon"]'
        ).click();

        queryAndAssert<GrConfirmAbandonDialog>(
          element,
          '#confirmAbandonDialog'
        ).message = 'foo message';
        element.handleAbandonDialogConfirm();
        assert.notOk(alertStub.called);

        const action = {
          __key: 'abandon',
          __type: 'change',
          __primary: false,
          enabled: true,
          label: 'Abandon',
          method: HttpMethod.POST,
          title: 'Abandon the change',
        };
        assert.deepEqual(fireActionStub.lastCall.args, [
          '/abandon',
          action,
          false,
          {
            message: 'foo message',
          },
        ]);
      });
    });

    suite('revert change', () => {
      let fireActionStub: sinon.SinonStub;

      setup(async () => {
        fireActionStub = sinon.stub(element, 'fireAction');
        element.commitMessage = 'random commit message';
        element.change = {
          ...createChangeViewChange(),
          current_revision: 'abcdef' as CommitId,
          actions: {
            revert: {
              method: HttpMethod.POST,
              label: 'Revert',
              title: 'Revert the change',
              enabled: true,
            },
          },
        };
        await element.updateComplete;
        // test
        await element.reload();
      });

      test('revert change payload', async () => {
        await element.updateComplete;
        queryAndAssert<GrButton>(
          element,
          'gr-button[data-action-key="revert"]'
        ).click();
        const revertAction = {
          __key: 'revert',
          __type: 'change',
          __primary: false,
          method: HttpMethod.POST,
          label: 'Revert',
          title: 'Revert the change',
          enabled: true,
        };
        queryAndAssert(element, 'gr-confirm-revert-dialog').dispatchEvent(
          new CustomEvent('confirm', {
            detail: {
              message: 'foo message',
              revertType: 1,
            },
          })
        );
        assert.deepEqual(fireActionStub.lastCall.args, [
          '/revert',
          assertUIActionInfo(revertAction),
          false,
          {
            message: 'foo message',
          },
        ]);
      });

      test('revert change with plugin hook', async () => {
        const newRevertMsg = 'Modified revert msg';
        sinon
          .stub(
            queryAndAssert<GrConfirmRevertDialog>(
              element,
              '#confirmRevertDialog'
            ),
            'modifyRevertMsg'
          )
          .callsFake(() => newRevertMsg);
        element.change = {
{'fraction_non_alphanumeric': 0.13064277916907469, 'fraction_numerical': 0.003692287934245168, 'mean_word_length': 2.0126063966984784, 'pattern_counts': {'":': 0, '<': 262, '<?xml version=': 0, '>': 403, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '421590', 'n_tokens_mistral': 26019, 'n_tokens_neox': 25573, 'n_words': 4508}