/**
 * @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 {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
import {
  createAccountWithId,
  createApproval,
  createChange,
  createChangeMessages,
  createChangeViewChange,
  createRevision,
  createRevisions,
} from '../../../test/test-data-generators';
import {ChangeStatus, HttpMethod} from '../../../constants/constants';
import {
  mockPromise,
  query,
  queryAll,
  queryAndAssert,
  spyStorage,
  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 {getAppContext} from '../../../services/app-context';
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 {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
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';

// 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(getPluginLoader(), '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>
          <gr-overlay
            aria-hidden="true"
            id="overlay"
            style="outline: none; display: none;"
            tabindex="-1"
            with-backdrop=""
          >
            <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>
          </gr-overlay>
        `
      );
    });

    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)
      );
      sinon
        .stub(queryAndAssert<GrOverlay>(element, '#overlay'), 'open')
        .returns(Promise.resolve());
      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)
      );
      sinon
        .stub(queryAndAssert<GrOverlay>(element, '#overlay'), 'open')
        .returns(Promise.resolve());
      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('fullscreen-overlay-opened hides content', () => {
      const spy = sinon.spy(element, 'handleHideBackgroundContent');
      queryAndAssert<GrOverlay>(element, '#overlay').dispatchEvent(
        new CustomEvent('fullscreen-overlay-opened', {
          composed: true,
          bubbles: true,
        })
      );
      assert.isTrue(spy.called);
      assert.isTrue(
        queryAndAssert<Element>(element, '#mainContent').classList.contains(
          'overlayOpen'
        )
      );
    });

    test('fullscreen-overlay-closed shows content', () => {
      const spy = sinon.spy(element, 'handleShowBackgroundContent');
      queryAndAssert<GrOverlay>(element, '#overlay').dispatchEvent(
        new CustomEvent('fullscreen-overlay-closed', {
          composed: true,
          bubbles: true,
        })
      );
      assert.isTrue(spy.called);
      assert.isFalse(
        queryAndAssert<Element>(element, '#mainContent').classList.contains(
          'overlayOpen'
        )
      );
    });

    test('setReviewOnRevert', () => {
      const review = {labels: {Foo: 1, 'Bar-Baz': -2}};
      const changeId = 1234 as NumericChangeId;
      sinon.stub(element.jsAPI, '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 = getAppContext().storageService;
        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 = spyStorage(
          '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 = {
          ...createChangeViewChange(),
          current_revision: 'abc1234' as CommitId,
          actions: {
            revert: {
              method: HttpMethod.POST,
              label: 'Revert',
              title: 'Revert the change',
              enabled: true,
            },
          },
        };
        stubRestApi('getChanges').returns(
          Promise.resolve([
            {
              ...createChange(),
              change_id: '12345678901234' as ChangeId,
              topic: 'T' as TopicName,
              subject: 'random',
            },
            {
              ...createChange(),
              change_id: '23456' as ChangeId,
              topic: 'T' as TopicName,
              subject: 'a'.repeat(100),
            },
          ])
        );
        sinon
          .stub(
            queryAndAssert<GrConfirmRevertDialog>(
              element,
              '#confirmRevertDialog'
            ),
            'populateRevertSubmissionMessage'
          )
          .callsFake(() => 'original msg');
        await element.updateComplete;
        queryAndAssert<GrButton>(
          element,
          'gr-button[data-action-key="revert"]'
        ).click();
        await element.updateComplete;
        assert.equal(
          queryAndAssert<GrConfirmRevertDialog>(element, '#confirmRevertDialog')
            .message,
          newRevertMsg
        );
      });

      suite('revert change submitted together', () => {
        let getChangesStub: sinon.SinonStub;
        setup(async () => {
          element.change = {
            ...createChangeViewChange(),
            submission_id: '199 0' as ChangeSubmissionId,
            current_revision: '2000' as CommitId,
            actions: {
              revert: {
                method: HttpMethod.POST,
                label: 'Revert',
                title: 'Revert the change',
                enabled: true,
              },
            },
          };
          getChangesStub = stubRestApi('getChanges').returns(
            Promise.resolve([
              {
                ...createChange(),
                change_id: '12345678901234' as ChangeId,
                topic: 'T' as TopicName,
                subject: 'random',
              },
              {
                ...createChange(),
                change_id: '23456' as ChangeId,
                topic: 'T' as TopicName,
                subject: 'a'.repeat(100),
              },
            ])
          );
          await element.updateComplete;
        });

        test('confirm revert dialog shows both options', async () => {
          queryAndAssert<GrButton>(
            element,
            'gr-button[data-action-key="revert"]'
          ).click();
          await element.updateComplete;
          assert.equal(getChangesStub.args[0][1], 'submissionid: "199 0"');
          const confirmRevertDialog = queryAndAssert<GrConfirmRevertDialog>(
            element,
            '#confirmRevertDialog'
          );
          await element.updateComplete;
          const revertSingleChangeLabel = queryAndAssert<HTMLLabelElement>(
            confirmRevertDialog,
            '.revertSingleChange'
          );
          const revertSubmissionLabel = queryAndAssert<HTMLLabelElement>(
            confirmRevertDialog,
            '.revertSubmission'
          );
          assert(
            revertSingleChangeLabel.innerText.trim() === 'Revert single change'
          );
          assert(
            revertSubmissionLabel.innerText.trim() ===
              'Revert entire submission (2 Changes)'
          );
          let expectedMsg =
            'Revert submission 199 0' +
            '\n\n' +
            'Reason for revert: <INSERT REASONING HERE>' +
            '\n' +
            'Reverted Changes:' +
            '\n' +
            '1234567890:random' +
            '\n' +
            '23456:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...' +
            '\n';
          assert.equal(confirmRevertDialog.message, expectedMsg);
          const radioInputs = queryAll<HTMLInputElement>(
            confirmRevertDialog,
            'input[name="revertOptions"]'
          );
          radioInputs[0].click();
          await element.updateComplete;
          expectedMsg =
            'Revert "random commit message"\n\nThis reverts ' +
            'commit 2000.\n\nReason' +
            ' for revert: <INSERT REASONING HERE>\n';
          assert.equal(confirmRevertDialog.message, expectedMsg);
        });

        test('submit fails if message is not edited', async () => {
          queryAndAssert<GrButton>(
            element,
            'gr-button[data-action-key="revert"]'
          ).click();
          const confirmRevertDialog = queryAndAssert<GrConfirmRevertDialog>(
            element,
            '#confirmRevertDialog'
          );
          const fireStub = sinon.stub(confirmRevertDialog, 'dispatchEvent');
          await element.updateComplete;
          queryAndAssert<GrButton>(
            queryAndAssert(
              queryAndAssert<GrConfirmRevertDialog>(
                element,
                '#confirmRevertDialog'
              ),
              'gr-dialog'
            ),
            '#confirm'
          ).click();
          await element.updateComplete;
          assert.isTrue(confirmRevertDialog.showErrorMessage);
          assert.isFalse(fireStub.called);
        });

        test('message modification is retained on switching', async () => {
          queryAndAssert<GrButton>(
            element,
            'gr-button[data-action-key="revert"]'
          ).click();
          await element.updateComplete;
          const confirmRevertDialog = queryAndAssert<GrConfirmRevertDialog>(
            element,
            '#confirmRevertDialog'
          );
          await element.updateComplete;
          const radioInputs = queryAll<HTMLInputElement>(
            confirmRevertDialog,
            'input[name="revertOptions"]'
          );
          const revertSubmissionMsg =
            'Revert submission 199 0' +
            '\n\n' +
            'Reason for revert: <INSERT REASONING HERE>' +
            '\n' +
            'Reverted Changes:' +
            '\n' +
            '1234567890:random' +
            '\n' +
            '23456:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...' +
            '\n';
          const singleChangeMsg =
            'Revert "random commit message"\n\nThis reverts ' +
            'commit 2000.\n\nReason' +
            ' for revert: <INSERT REASONING HERE>\n';
          assert.equal(confirmRevertDialog.message, revertSubmissionMsg);
          const newRevertMsg = revertSubmissionMsg + 'random';
          const newSingleChangeMsg = singleChangeMsg + 'random';
          confirmRevertDialog.message = newRevertMsg;
          await element.updateComplete;
          radioInputs[0].click();
          await element.updateComplete;
          assert.equal(confirmRevertDialog.message, singleChangeMsg);
          confirmRevertDialog.message = newSingleChangeMsg;
          await element.updateComplete;
          radioInputs[1].click();
          await element.updateComplete;
          assert.equal(confirmRevertDialog.message, newRevertMsg);
          radioInputs[0].click();
          await element.updateComplete;
          assert.equal(confirmRevertDialog.message, newSingleChangeMsg);
        });
      });

      suite('revert single change', () => {
        setup(async () => {
          element.change = {
            ...createChangeViewChange(),
            submission_id: '199' as ChangeSubmissionId,
            current_revision: '2000' as CommitId,
            actions: {
              revert: {
                method: HttpMethod.POST,
                label: 'Revert',
                title: 'Revert the change',
                enabled: true,
              },
            },
          };
          stubRestApi('getChanges').returns(
            Promise.resolve([
              {
                ...createChange(),
                change_id: '12345678901234' as ChangeId,
                topic: 'T' as TopicName,
                subject: 'random',
              },
            ])
          );
          await element.updateComplete;
        });

        test('submit fails if message is not edited', async () => {
          queryAndAssert<GrButton>(
            element,
            'gr-button[data-action-key="revert"]'
          ).click();
          const confirmRevertDialog = queryAndAssert<GrConfirmRevertDialog>(
            element,
            '#confirmRevertDialog'
          );
          const fireStub = sinon.stub(confirmRevertDialog, 'dispatchEvent');
          await element.updateComplete;
          queryAndAssert<GrButton>(
            queryAndAssert(
              queryAndAssert<GrConfirmRevertDialog>(
                element,
                '#confirmRevertDialog'
              ),
              'gr-dialog'
            ),
            '#confirm'
          ).click();
          await element.updateComplete;
          assert.isTrue(confirmRevertDialog.showErrorMessage);
          assert.isFalse(fireStub.called);
        });

        test('confirm revert dialog shows no radio button', async () => {
          queryAndAssert<GrButton>(
            element,
            'gr-button[data-action-key="revert"]'
          ).click();
          await element.updateComplete;
          const confirmRevertDialog = queryAndAssert<GrConfirmRevertDialog>(
            element,
            '#confirmRevertDialog'
          );
          const radioInputs = queryAll(
            confirmRevertDialog,
            'input[name="revertOptions"]'
          );
          assert.equal(radioInputs.length, 0);
          const msg =
            'Revert "random commit message"\n\n' +
            'This reverts commit 2000.\n\nReason ' +
            'for revert: <INSERT REASONING HERE>\n';
          assert.equal(confirmRevertDialog.message, msg);
          let editedMsg = msg + 'hello';
          confirmRevertDialog.message += 'hello';
          const confirmButton = queryAndAssert<GrButton>(
            queryAndAssert(
              queryAndAssert<GrConfirmRevertDialog>(
                element,
                '#confirmRevertDialog'
              ),
              'gr-dialog'
            ),
            '#confirm'
          );
          confirmButton.click();
          await element.updateComplete;
          // Contains generic template reason so doesn't submit
          assert.isFalse(fireActionStub.called);
          confirmRevertDialog.message = confirmRevertDialog.message.replace(
            '<INSERT REASONING HERE>',
            ''
          );
          editedMsg = editedMsg.replace('<INSERT REASONING HERE>', '');
          confirmButton.click();
          await element.updateComplete;
          assert.equal(fireActionStub.getCall(0).args[0], '/revert');
          assert.equal(fireActionStub.getCall(0).args[1].__key, 'revert');
          assert.equal(fireActionStub.getCall(0).args[3].message, editedMsg);
        });
      });
    });

    suite('mark change private', () => {
      setup(async () => {
        const privateAction = {
          __key: 'private',
          __type: 'change',
          __primary: false,
          method: HttpMethod.POST,
          label: 'Mark private',
          title: 'Working...',
          enabled: true,
        };

        element.actions = {
          private: privateAction,
        };

        element.change!.is_private = false;

        element.changeNum = 2 as NumericChangeId;
        element.latestPatchNum = 2 as PatchSetNum;

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

      test(
        'make sure the mark private change button is not outside of the ' +
          'overflow menu',
        async () => {
          await element.updateComplete;
          assert.isNotOk(query(element, '[data-action-key="private"]'));
        }
      );

      test('private change', async () => {
        await element.updateComplete;
        assert.isOk(
          query(
            queryAndAssert<GrDropdown>(element, '#moreActions'),
            'span[data-id="private-change"]'
          )
        );
        element.setActionOverflow(ActionType.CHANGE, 'private', false);
        await element.updateComplete;
        assert.isOk(query(element, '[data-action-key="private"]'));
        assert.isNotOk(
          query(
            queryAndAssert<GrDropdown>(element, '#moreActions'),
            'span[data-id="private-change"]'
          )
        );
      });
    });

    suite('unmark private change', () => {
      setup(async () => {
        const unmarkPrivateAction = {
          __key: 'private.delete',
          __type: 'change',
          __primary: false,
          method: HttpMethod.POST,
          label: 'Unmark private',
          title: 'Working...',
          enabled: true,
        };

        element.actions = {
          'private.delete': unmarkPrivateAction,
        };

        element.change!.is_private = true;

        element.changeNum = 2 as NumericChangeId;
        element.latestPatchNum = 2 as PatchSetNum;

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

      test(
        'make sure the unmark private change button is not outside of the ' +
          'overflow menu',
        async () => {
          await element.updateComplete;
          assert.isNotOk(query(element, '[data-action-key="private.delete"]'));
        }
      );

      test('unmark the private change', async () => {
        await element.updateComplete;
        assert.isOk(
          query(
            queryAndAssert<GrDropdown>(element, '#moreActions'),
            'span[data-id="private.delete-change"]'
          )
        );
        element.setActionOverflow(ActionType.CHANGE, 'private.delete', false);
        await element.updateComplete;
        assert.isOk(query(element, '[data-action-key="private.delete"]'));
        assert.isNotOk(
          query(
            queryAndAssert<GrDropdown>(element, '#moreActions'),
            'span[data-id="private.delete-change"]'
          )
        );
      });
    });

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

      setup(async () => {
        fireActionStub = sinon.stub(element, 'fireAction');
        deleteAction = {
          method: HttpMethod.DELETE,
          label: 'Delete Change',
          title: 'Delete change X_X',
          enabled: true,
        };
        element.change = {
          ...createChangeViewChange(),
          current_revision: 'abc1234' as CommitId,
          actions: {
            '/': deleteAction,
          },
        };
        await element.updateComplete;
      });

      test('does not delete on action', () => {
        element.handleDeleteTap();
        assert.isFalse(fireActionStub.called);
      });

      test('shows confirm dialog', async () => {
        element.handleDeleteTap();
        assert.isFalse(
          queryAndAssert<GrDialog>(element, '#confirmDeleteDialog').hidden
        );
        queryAndAssert<GrButton>(
          queryAndAssert(element, '#confirmDeleteDialog'),
          'gr-button[primary]'
        ).click();
        await element.updateComplete;
        assert.isTrue(fireActionStub.calledWith('/', deleteAction, false));
      });

      test('hides delete confirm on cancel', async () => {
        element.handleDeleteTap();
        queryAndAssert<GrButton>(
          queryAndAssert(element, '#confirmDeleteDialog'),
          'gr-button:not([primary])'
        ).click();
        await element.updateComplete;
        assert.isTrue(
          queryAndAssert<GrDialog>(element, '#confirmDeleteDialog').hidden
        );
        assert.isFalse(fireActionStub.called);
      });
    });

    suite('quick approve', () => {
      setup(async () => {
        element.change = {
          ...createChangeViewChange(),
          current_revision: 'abc1234' as CommitId,
          labels: {
            foo: {
              values: {
                '-1': '',
                ' 0': '',
                '+1': '',
              },
            },
          },
          permitted_labels: {
            foo: ['-1', ' 0', '+1'],
          },
        };
        await element.updateComplete;
      });

      test('added when can approve', () => {
        const approveButton = queryAndAssert(
          element,
          "gr-button[data-action-key='review']"
        );
        assert.isNotNull(approveButton);
      });

      test('hide quick approve', async () => {
        const approveButton = query(
          element,
          "gr-button[data-action-key='review']"
        );
        assert.isNotNull(approveButton);
        assert.isFalse(element._hideQuickApproveAction);

        // Assert approve button gets removed from list of buttons.
        element.hideQuickApproveAction();
        await element.updateComplete;
        const approveButtonUpdated = query(
          element,
          "gr-button[data-action-key='review']"
        );
        assert.isNotOk(approveButtonUpdated);
        assert.isTrue(element._hideQuickApproveAction);
      });

      test('is first in list of secondary actions', () => {
        const approveButton = queryAndAssert<HTMLElement>(
          element,
          '#secondaryActions'
        ).querySelector('gr-button');
        assert.equal(approveButton!.getAttribute('data-label'), 'foo+1');
      });

      test('not added when change is merged', async () => {
        element.change = {
          ...element.change!,
          status: ChangeStatus.MERGED,
        };

        await element.updateComplete;
        const approveButton = query(
          element,
          "gr-button[data-action-key='review']"
        );
        assert.isNotOk(approveButton);
      });

      test('not added when already approved', async () => {
        element.change = {
          ...createChangeViewChange(),
          current_revision: 'abc1234' as CommitId,
          labels: {
            foo: {
              approved: {},
              values: {},
            },
          },
          permitted_labels: {
            foo: [' 0', '+1'],
          },
        };
        await element.updateComplete;
        const approveButton = query(
          element,
          "gr-button[data-action-key='review']"
        );
        assert.isNotOk(approveButton);
      });

      test('not added when label not permitted', async () => {
        element.change = {
          ...createChangeViewChange(),
          current_revision: 'abc1234' as CommitId,
          labels: {
            foo: {values: {}},
          },
          permitted_labels: {
            bar: [],
          },
        };
        await element.updateComplete;
        const approveButton = query(
          element,
          "gr-button[data-action-key='review']"
        );
        assert.isNotOk(approveButton);
      });

      test('added even when label is optional', async () => {
        element.change = {
          ...createChangeViewChange(),
          current_revision: 'abc1234' as CommitId,
          labels: {
            'Code-Review': {
              optional: true,
              values: {
                '-1': '',
                ' 0': '',
                '+1': '',
              },
            },
          },
          permitted_labels: {
            'Code-Review': ['-1', ' 0', '+1'],
          },
        };
        await element.updateComplete;
        const approveButton = query(
          element,
          "gr-button[data-action-key='review']"
        );
        assert.isOk(approveButton);
      });

      test('approves when tapped', async () => {
        const fireActionStub = sinon.stub(element, 'fireAction');
        queryAndAssert<GrButton>(
          element,
          "gr-button[data-action-key='review']"
        ).click();
        await element.updateComplete;
        assert.isTrue(fireActionStub.called);
        assert.isTrue(fireActionStub.calledWith('/review'));
        const payload = fireActionStub.lastCall.args[3];
        assert.deepEqual((payload as ReviewInput).labels, {foo: 1});
      });

      test('not added when multiple labels are required without code review', async () => {
        element.change = {
          ...createChangeViewChange(),
          current_revision: 'abc1234' as CommitId,
          labels: {
            foo: {values: {}},
            bar: {values: {}},
          },
          permitted_labels: {
            foo: [' 0', '+1'],
            bar: [' 0', '+1', '+2'],
          },
        };
        await element.updateComplete;
        const approveButton = query(
          element,
          "gr-button[data-action-key='review']"
        );
        assert.isNotOk(approveButton);
      });

      test('code review shown with multiple missing approval', async () => {
        element.change = {
          ...createChangeViewChange(),
          current_revision: 'abc1234' as CommitId,
          labels: {
            foo: {values: {}},
            bar: {values: {}},
            'Code-Review': {
              approved: {},
              values: {
                ' 0': '',
                '+1': '',
                '+2': '',
              },
            },
          },
          permitted_labels: {
            foo: [' 0', '+1'],
            bar: [' 0', '+1', '+2'],
            'Code-Review': [' 0', '+1', '+2'],
          },
        };
        await element.updateComplete;
        const approveButton = queryAndAssert(
          element,
          "gr-button[data-action-key='review']"
        );
        assert.isOk(approveButton);
      });

      test('button label for missing approval', async () => {
        element.change = {
          ...createChangeViewChange(),
          current_revision: 'abc1234' as CommitId,
          labels: {
            foo: {
              values: {
                ' 0': '',
                '+1': '',
              },
            },
            bar: {approved: {}, values: {}},
          },
          permitted_labels: {
            foo: [' 0', '+1'],
            bar: [' 0', '+1', '+2'],
          },
        };
        await element.updateComplete;
        const approveButton = queryAndAssert(
          element,
          "gr-button[data-action-key='review']"
        );
        assert.equal(approveButton.getAttribute('data-label'), 'foo+1');
      });

      test('no quick approve if score is not maximal for a label', async () => {
        element.change = {
          ...createChangeViewChange(),
          current_revision: 'abc1234' as CommitId,
          labels: {
            bar: {
              value: 1,
              values: {
                ' 0': '',
                '+1': '',
                '+2': '',
              },
            },
          },
          permitted_labels: {
            bar: [' 0', '+1'],
          },
        };
        await element.updateComplete;
        const approveButton = query(
          element,
          "gr-button[data-action-key='review']"
        );
        assert.isNotOk(approveButton);
      });

      test('approving label with a non-max score', async () => {
        element.change = {
          ...createChangeViewChange(),
          current_revision: 'abc1234' as CommitId,
          labels: {
            bar: {
              value: 1,
              values: {
                ' 0': '',
                '+1': '',
                '+2': '',
              },
            },
          },
          permitted_labels: {
            bar: [' 0', '+1', '+2'],
          },
        };
        await element.updateComplete;
        const approveButton = queryAndAssert(
          element,
          "gr-button[data-action-key='review']"
        );
        assert.equal(approveButton.getAttribute('data-label'), 'bar+2');
      });

      test('added when can approve an already-approved code review label', async () => {
        element.change = {
          ...createChangeViewChange(),
          current_revision: 'abc1234' as CommitId,
          labels: {
            'Code-Review': {
              approved: {},
              values: {
                ' 0': '',
                '+1': '',
                '+2': '',
              },
            },
          },
          permitted_labels: {
            'Code-Review': [' 0', '+1', '+2'],
          },
        };
        await element.updateComplete;
        const approveButton = queryAndAssert<GrButton>(
          element,
          "gr-button[data-action-key='review']"
        );
        assert.isNotNull(approveButton);
      });

      test('not added when the user has already approved', async () => {
        const vote = {
          ...createApproval(),
          _account_id: 123 as AccountId,
          name: 'name',
          value: 2,
        };
        element.change = {
          ...createChangeViewChange(),
          current_revision: 'abc1234' as CommitId,
          labels: {
            'Code-Review': {
              approved: {},
              values: {
                ' 0': '',
                '+1': '',
                '+2': '',
              },
              all: [vote],
            },
          },
          permitted_labels: {
            'Code-Review': [' 0', '+1', '+2'],
          },
        };
        await element.updateComplete;
        const approveButton = query(
          element,
          "gr-button[data-action-key='review']"
        );
        assert.isNotOk(approveButton);
      });

      test('not added when user owns the change', async () => {
        element.change = {
          ...createChangeViewChange(),
          current_revision: 'abc1234' as CommitId,
          owner: createAccountWithId(123),
          labels: {
            'Code-Review': {
              approved: {},
              values: {
                ' 0': '',
                '+1': '',
                '+2': '',
              },
            },
          },
          permitted_labels: {
            'Code-Review': [' 0', '+1', '+2'],
          },
        };
        await element.updateComplete;
        const approveButton = query(
          element,
          "gr-button[data-action-key='review']"
        );
        assert.isNotOk(approveButton);
      });
    });

    test('adds download revision action', async () => {
      const handler = sinon.stub();
      element.addEventListener('download-tap', handler);
      assert.ok(element.revisionActions.download);
      element.handleDownloadTap();
      await element.updateComplete;

      assert.isTrue(handler.called);
    });

    test('changing changeNum or patchNum does not reload', () => {
      const reloadStub = sinon.stub(element, 'reload');
      element.changeNum = 123 as NumericChangeId;
      assert.isFalse(reloadStub.called);
      element.latestPatchNum = 456 as PatchSetNum;
      assert.isFalse(reloadStub.called);
    });

    test('toSentenceCase', () => {
      assert.equal(element.toSentenceCase('blah blah'), 'Blah blah');
      assert.equal(element.toSentenceCase('BLAH BLAH'), 'Blah blah');
      assert.equal(element.toSentenceCase('b'), 'B');
      assert.equal(element.toSentenceCase(''), '');
      assert.equal(element.toSentenceCase('!@#$%^&*()'), '!@#$%^&*()');
    });

    suite('setActionOverflow', () => {
      test('move action from overflow', async () => {
        assert.isNotOk(query(element, '[data-action-key="cherrypick"]'));
        assert.strictEqual(
          queryAndAssert<GrDropdown>(element, '#moreActions').items![0].id,
          'cherrypick-revision'
        );
        element.setActionOverflow(ActionType.REVISION, 'cherrypick', false);
        await element.updateComplete;
        assert.isOk(query(element, '[data-action-key="cherrypick"]'));
        assert.notEqual(
          queryAndAssert<GrDropdown>(element, '#moreActions').items![0].id,
          'cherrypick-revision'
        );
      });

      test('move action to overflow', async () => {
        assert.isOk(query(element, '[data-action-key="submit"]'));
        element.setActionOverflow(ActionType.REVISION, 'submit', true);
        await element.updateComplete;
        assert.isNotOk(query(element, '[data-action-key="submit"]'));
        assert.strictEqual(
          queryAndAssert<GrDropdown>(element, '#moreActions').items![3].id,
          'submit-revision'
        );
      });

      suite('waitForChangeReachable', () => {
        let clock: SinonFakeTimers;
        setup(() => {
          clock = sinon.useFakeTimers();
        });

        const makeGetChange = (numTries: number) => () => {
          if (numTries === 1) {
            return Promise.resolve({
              ...createChangeViewChange(),
              _number: 123 as NumericChangeId,
            });
          } else {
            numTries--;
            return Promise.resolve(null);
          }
        };

        const tickAndFlush = async (repetitions: number) => {
          for (let i = 1; i <= repetitions; i++) {
            clock.tick(1000);
            await element.updateComplete;
          }
        };

        test('succeed', async () => {
          stubRestApi('getChange').callsFake(makeGetChange(5));
          const promise = element.waitForChangeReachable(
            123 as NumericChangeId
          );
          tickAndFlush(5);
          const success = await promise;
          assert.isTrue(success);
        });

        test('fail', async () => {
          stubRestApi('getChange').callsFake(makeGetChange(6));
          const promise = element.waitForChangeReachable(
            123 as NumericChangeId
          );
          tickAndFlush(6);
          const success = await promise;
          assert.isFalse(success);
        });
      });
    });

    suite('send', () => {
      let cleanup: sinon.SinonStub;
      const payload = {foo: 'bar'};
      let onShowError: sinon.SinonStub;
      let onShowAlert: sinon.SinonStub;
      let getResponseObjectStub: sinon.SinonStub;

      setup(async () => {
        cleanup = sinon.stub();
        element.changeNum = 42 as NumericChangeId;
        element.latestPatchNum = 12 as PatchSetNum;
        element.change = {
          ...createChangeViewChange(),
          revisions: createRevisions(element.latestPatchNum as number),
          messages: createChangeMessages(1),
        };
        element.change._number = 42 as NumericChangeId;
        await element.updateComplete;

        onShowError = sinon.stub();
        element.addEventListener('show-error', onShowError);
        onShowAlert = sinon.stub();
        element.addEventListener(EventType.SHOW_ALERT, onShowAlert);
      });

      suite('happy path', () => {
        let sendStub: sinon.SinonStub;
        setup(() => {
          stubRestApi('getChangeDetail').returns(
            Promise.resolve({
              ...createChangeViewChange(),
              // element has latest info
              revisions: createRevisions(element.latestPatchNum as number),
              messages: createChangeMessages(1),
            })
          );
          getResponseObjectStub = stubRestApi('getResponseObject');
          sendStub = stubRestApi('executeChangeAction').returns(
            Promise.resolve(new Response())
          );
        });

        test('change action', async () => {
          await element.send(
            HttpMethod.DELETE,
            payload,
            '/endpoint',
            false,
            cleanup,
            {} as UIActionInfo
          );
          assert.isFalse(onShowError.called);
          assert.isTrue(cleanup.calledOnce);
          assert.isTrue(
            sendStub.calledWith(
              42,
              HttpMethod.DELETE,
              '/endpoint',
              undefined,
              payload
            )
          );
        });

        suite('show revert submission dialog', () => {
          setup(async () => {
            element.change!.submission_id = '199' as ChangeSubmissionId;
            element.change!.current_revision = '2000' as CommitId;
            stubRestApi('getChanges').returns(
              Promise.resolve([
                {
                  ...createChangeViewChange(),
                  change_id: '12345678901234' as ChangeId,
                  topic: 'T' as TopicName,
                  subject: 'random',
                },
                {
                  ...createChangeViewChange(),
                  change_id: '23456' as ChangeId,
                  topic: 'T' as TopicName,
                  subject: 'a'.repeat(100),
                },
              ])
            );
            await element.updateComplete;
          });
        });

        suite('single changes revert', () => {
          let setUrlStub: sinon.SinonStub;
          setup(() => {
            getResponseObjectStub.returns(
              Promise.resolve({
                revert_changes: [{change_id: 12345, topic: 'T'}],
              })
            );
            setUrlStub = sinon.stub(testResolver(navigationToken), 'setUrl');
          });

          test('revert submission single change', async () => {
            await element.send(
              HttpMethod.POST,
              {message: 'Revert submission'},
              '/revert_submission',
              false,
              cleanup,
              {} as UIActionInfo
            );
            await element.handleResponse(
              {
                __key: 'revert_submission',
                __type: ActionType.CHANGE,
                label: 'l',
              },
              new Response()
            );
            assert.isTrue(setUrlStub.called);
            assert.equal(setUrlStub.lastCall.args[0], '/q/topic:T');
          });
        });

        suite('multiple changes revert', () => {
          let showActionDialogStub: sinon.SinonStub;
          let setUrlStub: sinon.SinonStub;
          setup(() => {
            getResponseObjectStub.returns(
              Promise.resolve({
                revert_changes: [
                  {change_id: 12345, topic: 'T'},
                  {change_id: 23456, topic: 'T'},
                ],
              })
            );
            showActionDialogStub = sinon.stub(element, 'showActionDialog');
            setUrlStub = sinon.stub(testResolver(navigationToken), 'setUrl');
          });

          test('revert submission multiple change', async () => {
            await element.send(
              HttpMethod.POST,
              {message: 'Revert submission'},
              '/revert_submission',
              false,
              cleanup,
              {} as UIActionInfo
            );
            await element.handleResponse(
              {
                __key: 'revert_submission',
                __type: ActionType.CHANGE,
                label: 'l',
              },
              new Response()
            );
            assert.isFalse(showActionDialogStub.called);
            assert.isTrue(setUrlStub.called);
            assert.equal(setUrlStub.lastCall.args[0], '/q/topic:T');
          });
        });

        test('revision action', async () => {
          await element.send(
            HttpMethod.DELETE,
            payload,
            '/endpoint',
            true,
            cleanup,
            {} as UIActionInfo
          );
          assert.isFalse(onShowError.called);
          assert.isTrue(cleanup.calledOnce);
          assert.isTrue(
            sendStub.calledWith(42, 'DELETE', '/endpoint', 12, payload)
          );
        });
      });

      suite('failure modes', () => {
        test('non-latest', () => {
          stubRestApi('getChangeDetail').returns(
            Promise.resolve({
              ...createChangeViewChange(),
              // new patchset was uploaded
              revisions: createRevisions(
                (element.latestPatchNum as number) + 1
              ),
              messages: createChangeMessages(1),
            })
          );
          const sendStub = stubRestApi('executeChangeAction');

          return element
            .send(
              HttpMethod.DELETE,
              payload,
              '/endpoint',
              true,
              cleanup,
              {} as UIActionInfo
            )
            .then(() => {
              assert.isTrue(onShowAlert.calledOnce);
              assert.isFalse(onShowError.called);
              assert.isTrue(cleanup.calledOnce);
              assert.isFalse(sendStub.called);
            });
        });

        test('send fails', () => {
          stubRestApi('getChangeDetail').returns(
            Promise.resolve({
              ...createChangeViewChange(),
              // element has latest info
              revisions: createRevisions(element.latestPatchNum as number),
              messages: createChangeMessages(1),
            })
          );
          const sendStub = stubRestApi('executeChangeAction').callsFake(
            (_num, _method, _patchNum, _endpoint, _payload, onErr) => {
              // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
              onErr!();
              return Promise.resolve(undefined);
            }
          );
          const handleErrorStub = sinon.stub(element, 'handleResponseError');

          return element
            .send(
              HttpMethod.DELETE,
              payload,
              '/endpoint',
              true,
              cleanup,
              {} as UIActionInfo
            )
            .then(() => {
              assert.isFalse(onShowError.called);
              assert.isTrue(cleanup.called);
              assert.isTrue(sendStub.calledOnce);
              assert.isTrue(handleErrorStub.called);
            });
        });
      });
    });

    test('handleAction reports', () => {
      sinon.stub(element, 'fireAction');
      sinon.stub(element, 'handleChangeAction');

      const reportStub = stubReporting('reportInteraction');
      element.handleAction(ActionType.CHANGE, 'key');
      assert.isTrue(reportStub.called);
      assert.equal(reportStub.lastCall.args[0], 'change-key');
    });
  });

  suite('getChangeRevisionActions returns only some actions', () => {
    let element: GrChangeActions;

    let changeRevisionActions: ActionNameToActionInfoMap = {};

    setup(async () => {
      stubRestApi('getChangeRevisionActions').returns(
        Promise.resolve(changeRevisionActions)
      );
      stubRestApi('send').returns(Promise.reject(new Error('error')));

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

      element = await fixture<GrChangeActions>(html`
        <gr-change-actions></gr-change-actions>
      `);
      // getChangeRevisionActions is not called without
      // set the following properties
      element.change = createChangeViewChange();
      element.changeNum = 42 as NumericChangeId;
      element.latestPatchNum = 2 as PatchSetNum;

      stubRestApi('getRepoBranches').returns(Promise.resolve([]));
      await element.updateComplete;
      await element.reload();
    });

    test('confirmSubmitDialog and confirmRebase properties are changed', () => {
      changeRevisionActions = {};
      element.reload();
      assert.strictEqual(
        queryAndAssert<GrConfirmSubmitDialog>(element, '#confirmSubmitDialog')
          .action,
        null
      );
      assert.strictEqual(
        queryAndAssert<GrConfirmRebaseDialog>(element, '#confirmRebase')
          .rebaseOnCurrent,
        null
      );
    });
  });
});
