Convert gr-change-actions_test to typescript

Change-Id: I1edf170a2bf5cfacabdcc8a2a07e71a12886ff3a
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
index 4ae3c68..9199cea 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
@@ -111,6 +111,7 @@
   RevisionActions,
 } from '../../../api/change-actions';
 import {ErrorCallback} from '../../../api/rest';
+import {GrDropdown} from '../../shared/gr-dropdown/gr-dropdown';
 
 const ERR_BRANCH_EMPTY = 'The destination branch can’t be empty.';
 const ERR_COMMIT_EMPTY = 'The commit message can’t be empty.';
@@ -274,7 +275,7 @@
   ChangeActions.UNREVIEWED,
 ];
 
-function assertUIActionInfo(action?: ActionInfo): UIActionInfo {
+export function assertUIActionInfo(action?: ActionInfo): UIActionInfo {
   // TODO(TS): Remove this function. The gr-change-actions adds properties
   // to existing ActionInfo objects instead of creating a new objects. This
   // function checks, that 'action' has all property required by UIActionInfo.
@@ -332,6 +333,8 @@
     createFollowUpChange: GrCreateChangeDialog;
     confirmDeleteDialog: GrDialog;
     confirmDeleteEditDialog: GrDialog;
+    moreActions: GrDropdown;
+    secondaryActions: HTMLElement;
   };
 }
 
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.js
deleted file mode 100644
index c193e60..0000000
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.js
+++ /dev/null
@@ -1,2186 +0,0 @@
-/**
- * @license
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../../../test/common-test-setup-karma.js';
-import './gr-change-actions.js';
-import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
-import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
-import {
-  createAccountWithId,
-  createApproval,
-  createChange,
-  createChangeMessages,
-  createRevisions,
-} from '../../../test/test-data-generators.js';
-import {ChangeStatus} from '../../../constants/constants.js';
-import {stubRestApi} from '../../../test/test-utils.js';
-
-const basicFixture = fixtureFromElement('gr-change-actions');
-
-const CHERRY_PICK_TYPES = {
-  SINGLE_CHANGE: 1,
-  TOPIC: 2,
-};
-// TODO(dhruvsri): remove use of _populateRevertMessage as it's private
-suite('gr-change-actions tests', () => {
-  let element;
-
-  suite('basic tests', () => {
-    setup(() => {
-      stubRestApi('getChangeRevisionActions').returns(Promise.resolve({
-        cherrypick: {
-          method: 'POST',
-          label: 'Cherry Pick',
-          title: 'Cherry pick change to a different branch',
-          enabled: true,
-        },
-        rebase: {
-          method: 'POST',
-          label: 'Rebase',
-          title: 'Rebase onto tip of branch or parent change',
-          enabled: true,
-        },
-        submit: {
-          method: 'POST',
-          label: 'Submit',
-          title: 'Submit patch set 2 into master',
-          enabled: true,
-        },
-        revert_submission: {
-          method: 'POST',
-          label: 'Revert submission',
-          title: 'Revert this submission',
-          enabled: true,
-        },
-      }));
-      stubRestApi('send').callsFake((method, url, payload) => {
-        if (method !== 'POST') {
-          return Promise.reject(new Error('bad method'));
-        }
-        if (url === '/changes/test~42/revisions/2/submit') {
-          return Promise.resolve({
-            ok: true,
-            text() { return Promise.resolve(')]}\'\n{}'); },
-          });
-        } else if (url === '/changes/test~42/revisions/2/rebase') {
-          return Promise.resolve({
-            ok: true,
-            text() { return Promise.resolve(')]}\'\n{}'); },
-          });
-        }
-        return Promise.reject(new Error('bad url'));
-      });
-      stubRestApi('getProjectConfig').returns(Promise.resolve({}));
-
-      sinon.stub(getPluginLoader(), 'awaitPluginsLoaded')
-          .returns(Promise.resolve());
-
-      element = basicFixture.instantiate();
-      element.change = {};
-      element.changeNum = '42';
-      element.latestPatchNum = '2';
-      element.actions = {
-        '/': {
-          method: 'DELETE',
-          label: 'Delete Change',
-          title: 'Delete change X_X',
-          enabled: true,
-        },
-      };
-      element.account = {
-        _account_id: 123,
-      };
-      stubRestApi('getRepoBranches').returns(Promise.resolve([]));
-
-      return element.reload();
-    });
-
-    test('show-revision-actions event should fire', done => {
-      const spy = sinon.spy(element, '_sendShowRevisionActions');
-      element.reload();
-      flush(() => {
-        assert.isTrue(spy.called);
-        done();
-      });
-    });
-
-    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('_shouldHideActions', () => {
-      assert.isTrue(element._shouldHideActions(undefined, true));
-      assert.isTrue(element._shouldHideActions({base: {}}, false));
-      assert.isFalse(element._shouldHideActions({base: ['test']}, false));
-    });
-
-    test('plugin revision actions', done => {
-      const stub = stubRestApi('getChangeActionURL').returns(
-          Promise.resolve('the-url'));
-      element.revisionActions = {
-        'plugin~action': {},
-      };
-      assert.isOk(element.revisionActions['plugin~action']);
-      flush(() => {
-        assert.isTrue(stub.calledWith(
-            element.changeNum, element.latestPatchNum, '/plugin~action'));
-        assert.equal(element.revisionActions['plugin~action'].__url, 'the-url');
-        done();
-      });
-    });
-
-    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 flush();
-      assert.isTrue(stub.calledWith(
-          element.changeNum, undefined, '/plugin~action'));
-      assert.equal(element.actions['plugin~action'].__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', done => {
-      flush(() => {
-        const buttonEl = element.shadowRoot
-            .querySelector('[data-action-key="submit"]');
-        assert.isOk(buttonEl);
-        assert.throws(element.setActionHidden.bind(element, 'invalid type'));
-        element.setActionHidden(element.ActionType.REVISION,
-            element.RevisionActions.SUBMIT, true);
-        assert.lengthOf(element._hiddenActions, 1);
-        element.setActionHidden(element.ActionType.REVISION,
-            element.RevisionActions.SUBMIT, true);
-        assert.lengthOf(element._hiddenActions, 1);
-        flush(() => {
-          const buttonEl = element.shadowRoot
-              .querySelector('[data-action-key="submit"]');
-          assert.isNotOk(buttonEl);
-
-          element.setActionHidden(element.ActionType.REVISION,
-              element.RevisionActions.SUBMIT, false);
-          flush(() => {
-            const buttonEl = element.shadowRoot
-                .querySelector('[data-action-key="submit"]');
-            assert.isOk(buttonEl);
-            assert.isFalse(buttonEl.hasAttribute('hidden'));
-            done();
-          });
-        });
-      });
-    });
-
-    test('buttons exist', done => {
-      element._loading = false;
-      flush(() => {
-        const buttonEls = dom(element.root)
-            .querySelectorAll('gr-button');
-        const menuItems = 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);
-        done();
-      });
-    });
-
-    test('delete buttons have explicit labels', done => {
-      flush(() => {
-        const deleteItems = element.$.moreActions.items
-            .filter(item => item.id.startsWith('delete'));
-        assert.equal(deleteItems.length, 1);
-        assert.notEqual(deleteItems[0].name);
-        assert.equal(deleteItems[0].name, 'Delete change');
-        done();
-      });
-    });
-
-    test('get revision object from change', () => {
-      const revObj = {_number: 2, foo: 'bar'};
-      const change = {
-        revisions: {
-          rev1: {_number: 1},
-          rev2: revObj,
-        },
-      };
-      assert.deepEqual(element._getRevision(change, 2), revObj);
-    });
-
-    test('_actionComparator sort order', () => {
-      const actions = [
-        {label: '123', __type: 'change', __key: 'review'},
-        {label: 'abc-ro', __type: 'revision'},
-        {label: 'abc', __type: 'change'},
-        {label: 'def', __type: 'change'},
-        {label: 'def-p', __type: 'change', __primary: true},
-      ];
-
-      const result = actions.slice();
-      result.reverse();
-      result.sort(element._actionComparator.bind(element));
-      assert.deepEqual(result, actions);
-    });
-
-    test('submit change', () => {
-      const showSpy = sinon.spy(element, '_showActionDialog');
-      stubRestApi('getFromProjectLookup')
-          .returns(Promise.resolve('test'));
-      sinon.stub(element.$.overlay, 'open').returns(Promise.resolve());
-      element.change = {
-        revisions: {
-          rev1: {_number: 1},
-          rev2: {_number: 2},
-        },
-      };
-      element.latestPatchNum = '2';
-
-      const submitButton = element.shadowRoot
-          .querySelector('gr-button[data-action-key="submit"]');
-      assert.ok(submitButton);
-      MockInteractions.tap(submitButton);
-
-      flush();
-      assert.isTrue(showSpy.calledWith(element.$.confirmSubmitDialog));
-    });
-
-    test('submit change, tap on icon', done => {
-      sinon.stub(element.$.confirmSubmitDialog, 'resetFocus').callsFake( done);
-      stubRestApi('getFromProjectLookup')
-          .returns(Promise.resolve('test'));
-      sinon.stub(element.$.overlay, 'open').returns(Promise.resolve());
-      element.change = {
-        revisions: {
-          rev1: {_number: 1},
-          rev2: {_number: 2},
-        },
-      };
-      element.latestPatchNum = '2';
-
-      const submitIcon =
-          element.shadowRoot
-              .querySelector('gr-button[data-action-key="submit"] iron-icon');
-      assert.ok(submitIcon);
-      MockInteractions.tap(submitIcon);
-    });
-
-    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', 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', done => {
-      sinon.stub(element, '_canSubmitChange').callsFake(
-          () => false);
-      const fireActionStub = sinon.stub(element, '_fireAction');
-      flush(() => {
-        const submitButton = element.shadowRoot
-            .querySelector('gr-button[data-action-key="submit"]');
-        assert.ok(submitButton);
-        MockInteractions.tap(submitButton);
-        assert.equal(fireActionStub.callCount, 0);
-
-        done();
-      });
-    });
-
-    test('chain state', () => {
-      assert.equal(element._hasKnownChainState, false);
-      element.hasParent = true;
-      assert.equal(element._hasKnownChainState, true);
-      element.hasParent = false;
-    });
-
-    test('_calculateDisabled', () => {
-      let hasKnownChainState = false;
-      const action = {__key: 'rebase', enabled: true};
-      assert.equal(
-          element._calculateDisabled(action, hasKnownChainState), true);
-
-      action.__key = 'delete';
-      assert.equal(
-          element._calculateDisabled(action, hasKnownChainState), false);
-
-      action.__key = 'rebase';
-      hasKnownChainState = true;
-      assert.equal(
-          element._calculateDisabled(action, hasKnownChainState), false);
-
-      action.enabled = false;
-      assert.equal(
-          element._calculateDisabled(action, hasKnownChainState), false);
-    });
-
-    test('rebase change', done => {
-      const fireActionStub = sinon.stub(element, '_fireAction');
-      const fetchChangesStub = sinon.stub(element.$.confirmRebase,
-          'fetchRecentChanges').returns(Promise.resolve([]));
-      element._hasKnownChainState = true;
-      flush(() => {
-        const rebaseButton = element.shadowRoot
-            .querySelector('gr-button[data-action-key="rebase"]');
-        MockInteractions.tap(rebaseButton);
-        const rebaseAction = {
-          __key: 'rebase',
-          __type: 'revision',
-          __primary: false,
-          enabled: true,
-          label: 'Rebase',
-          method: 'POST',
-          title: 'Rebase onto tip of branch or parent change',
-        };
-        assert.isTrue(fetchChangesStub.called);
-        element._handleRebaseConfirm({detail: {base: '1234'}});
-        assert.deepEqual(fireActionStub.lastCall.args,
-            ['/rebase', rebaseAction, true, {base: '1234'}]);
-        done();
-      });
-    });
-
-    test('rebase change fires reload event', done => {
-      const eventStub = sinon.stub(element, 'dispatchEvent');
-      stubRestApi('getResponseObject').returns(
-          Promise.resolve({}));
-      element._handleResponse({__key: 'rebase'}, {});
-      flush(() => {
-        assert.isTrue(eventStub.called);
-        assert.equal(eventStub.lastCall.args[0].type, 'reload');
-        done();
-      });
-    });
-
-    test(`rebase dialog gets recent changes each time it's opened`, done => {
-      const fetchChangesStub = sinon.stub(element.$.confirmRebase,
-          'fetchRecentChanges').returns(Promise.resolve([]));
-      element._hasKnownChainState = true;
-      const rebaseButton = element.shadowRoot
-          .querySelector('gr-button[data-action-key="rebase"]');
-      MockInteractions.tap(rebaseButton);
-      assert.isTrue(fetchChangesStub.calledOnce);
-
-      flush(() => {
-        element.$.confirmRebase.dispatchEvent(
-            new CustomEvent('cancel', {
-              composed: true, bubbles: true,
-            }));
-        MockInteractions.tap(rebaseButton);
-        assert.isTrue(fetchChangesStub.calledTwice);
-        done();
-      });
-    });
-
-    test('two dialogs are not shown at the same time', async () => {
-      element._hasKnownChainState = true;
-      await flush();
-      const rebaseButton = element.shadowRoot
-          .querySelector('gr-button[data-action-key="rebase"]');
-      assert.ok(rebaseButton);
-      MockInteractions.tap(rebaseButton);
-      await flush();
-      assert.isFalse(element.$.confirmRebase.hidden);
-      stubRestApi('getChanges')
-          .returns(Promise.resolve([]));
-      element._handleCherrypickTap();
-      await flush();
-      assert.isTrue(element.$.confirmRebase.hidden);
-      assert.isFalse(element.$.confirmCherrypick.hidden);
-    });
-
-    test('fullscreen-overlay-opened hides content', () => {
-      sinon.spy(element, '_handleHideBackgroundContent');
-      element.$.overlay.dispatchEvent(
-          new CustomEvent('fullscreen-overlay-opened', {
-            composed: true, bubbles: true,
-          }));
-      assert.isTrue(element._handleHideBackgroundContent.called);
-      assert.isTrue(element.$.mainContent.classList.contains('overlayOpen'));
-    });
-
-    test('fullscreen-overlay-closed shows content', () => {
-      sinon.spy(element, '_handleShowBackgroundContent');
-      element.$.overlay.dispatchEvent(
-          new CustomEvent('fullscreen-overlay-closed', {
-            composed: true, bubbles: true,
-          }));
-      assert.isTrue(element._handleShowBackgroundContent.called);
-      assert.isFalse(element.$.mainContent.classList.contains('overlayOpen'));
-    });
-
-    test('_setReviewOnRevert', () => {
-      const review = {labels: {'Foo': 1, 'Bar-Baz': -2}};
-      const changeId = 1234;
-      sinon.stub(element.jsAPI, 'getReviewPostRevert').returns(review);
-      const saveStub = stubRestApi('saveChangeReview')
-          .returns(Promise.resolve());
-      return element._setReviewOnRevert(changeId).then(() => {
-        assert.isTrue(saveStub.calledOnce);
-        assert.equal(saveStub.lastCall.args[0], changeId);
-        assert.deepEqual(saveStub.lastCall.args[2], review);
-      });
-    });
-
-    suite('change edits', () => {
-      test('disableEdit', () => {
-        element.set('editMode', false);
-        element.set('editPatchsetLoaded', false);
-        element.change = {status: 'NEW'};
-        element.set('disableEdit', true);
-        flush();
-
-        assert.isNotOk(element.shadowRoot
-            .querySelector('gr-button[data-action-key="publishEdit"]'));
-        assert.isNotOk(element.shadowRoot
-            .querySelector('gr-button[data-action-key="rebaseEdit"]'));
-        assert.isNotOk(element.shadowRoot
-            .querySelector('gr-button[data-action-key="deleteEdit"]'));
-        assert.isNotOk(element.shadowRoot
-            .querySelector('gr-button[data-action-key="edit"]'));
-        assert.isNotOk(element.shadowRoot
-            .querySelector('gr-button[data-action-key="stopEdit"]'));
-      });
-
-      test('shows confirm dialog for delete edit', () => {
-        element.set('editMode', true);
-        element.set('editPatchsetLoaded', true);
-
-        const fireActionStub = sinon.stub(element, '_fireAction');
-        element._handleDeleteEditTap();
-        assert.isFalse(element.$.confirmDeleteEditDialog.hidden);
-        MockInteractions.tap(
-            element.shadowRoot
-                .querySelector('#confirmDeleteEditDialog')
-                .shadowRoot
-                .querySelector('gr-button[primary]'));
-        flush();
-
-        assert.equal(fireActionStub.lastCall.args[0], '/edit');
-      });
-
-      test('hide publishEdit and rebaseEdit if change is not open', () => {
-        element.set('editMode', true);
-        element.set('editPatchsetLoaded', true);
-        element.change = {status: 'MERGED'};
-        flush();
-
-        assert.isNotOk(element.shadowRoot
-            .querySelector('gr-button[data-action-key="publishEdit"]'));
-        assert.isNotOk(element.shadowRoot
-            .querySelector('gr-button[data-action-key="rebaseEdit"]'));
-        assert.isOk(element.shadowRoot
-            .querySelector('gr-button[data-action-key="deleteEdit"]'));
-        assert.isNotOk(element.shadowRoot
-            .querySelector('gr-button[data-action-key="edit"]'));
-      });
-
-      test('edit patchset is loaded, needs rebase', () => {
-        element.set('editMode', true);
-        element.set('editPatchsetLoaded', true);
-        element.change = {status: 'NEW'};
-        element.editBasedOnCurrentPatchSet = false;
-        flush();
-
-        assert.isNotOk(element.shadowRoot
-            .querySelector('gr-button[data-action-key="publishEdit"]'));
-        assert.isOk(element.shadowRoot
-            .querySelector('gr-button[data-action-key="rebaseEdit"]'));
-        assert.isOk(element.shadowRoot
-            .querySelector('gr-button[data-action-key="deleteEdit"]'));
-        assert.isNotOk(element.shadowRoot
-            .querySelector('gr-button[data-action-key="edit"]'));
-        assert.isNotOk(element.shadowRoot
-            .querySelector('gr-button[data-action-key="stopEdit"]'));
-      });
-
-      test('edit patchset is loaded, does not need rebase', () => {
-        element.set('editMode', true);
-        element.set('editPatchsetLoaded', true);
-        element.change = {status: 'NEW'};
-        element.editBasedOnCurrentPatchSet = true;
-        flush();
-
-        assert.isOk(element.shadowRoot
-            .querySelector('gr-button[data-action-key="publishEdit"]'));
-        assert.isNotOk(element.shadowRoot
-            .querySelector('gr-button[data-action-key="rebaseEdit"]'));
-        assert.isOk(element.shadowRoot
-            .querySelector('gr-button[data-action-key="deleteEdit"]'));
-        assert.isNotOk(element.shadowRoot
-            .querySelector('gr-button[data-action-key="edit"]'));
-        assert.isNotOk(element.shadowRoot
-            .querySelector('gr-button[data-action-key="stopEdit"]'));
-      });
-
-      test('edit mode is loaded, no edit patchset', () => {
-        element.set('editMode', true);
-        element.set('editPatchsetLoaded', false);
-        element.change = {status: 'NEW'};
-        flush();
-
-        assert.isNotOk(element.shadowRoot
-            .querySelector('gr-button[data-action-key="publishEdit"]'));
-        assert.isNotOk(element.shadowRoot
-            .querySelector('gr-button[data-action-key="rebaseEdit"]'));
-        assert.isNotOk(element.shadowRoot
-            .querySelector('gr-button[data-action-key="deleteEdit"]'));
-        assert.isNotOk(element.shadowRoot
-            .querySelector('gr-button[data-action-key="edit"]'));
-        assert.isOk(element.shadowRoot
-            .querySelector('gr-button[data-action-key="stopEdit"]'));
-      });
-
-      test('normal patch set', () => {
-        element.set('editMode', false);
-        element.set('editPatchsetLoaded', false);
-        element.change = {status: 'NEW'};
-        flush();
-
-        assert.isNotOk(element.shadowRoot
-            .querySelector('gr-button[data-action-key="publishEdit"]'));
-        assert.isNotOk(element.shadowRoot
-            .querySelector('gr-button[data-action-key="rebaseEdit"]'));
-        assert.isNotOk(element.shadowRoot
-            .querySelector('gr-button[data-action-key="deleteEdit"]'));
-        assert.isOk(element.shadowRoot
-            .querySelector('gr-button[data-action-key="edit"]'));
-        assert.isNotOk(element.shadowRoot
-            .querySelector('gr-button[data-action-key="stopEdit"]'));
-      });
-
-      test('edit action', done => {
-        element.addEventListener('edit-tap', () => { done(); });
-        element.set('editMode', true);
-        element.change = {status: 'NEW'};
-        flush();
-
-        assert.isNotOk(element.shadowRoot
-            .querySelector('gr-button[data-action-key="edit"]'));
-        assert.isOk(element.shadowRoot
-            .querySelector('gr-button[data-action-key="stopEdit"]'));
-        element.change = {status: 'MERGED'};
-        flush();
-
-        assert.isNotOk(element.shadowRoot
-            .querySelector('gr-button[data-action-key="edit"]'));
-        element.change = {status: 'NEW'};
-        element.set('editMode', false);
-        flush();
-
-        const editButton = element.shadowRoot
-            .querySelector('gr-button[data-action-key="edit"]');
-        assert.isOk(editButton);
-        MockInteractions.tap(editButton);
-      });
-    });
-
-    suite('cherry-pick', () => {
-      let fireActionStub;
-
-      setup(() => {
-        fireActionStub = sinon.stub(element, '_fireAction');
-        sinon.stub(window, 'alert');
-      });
-
-      test('works', () => {
-        element._handleCherrypickTap();
-        const action = {
-          __key: 'cherrypick',
-          __type: 'revision',
-          __primary: false,
-          enabled: true,
-          label: 'Cherry pick',
-          method: 'POST',
-          title: 'Cherry pick change to a different branch',
-        };
-
-        element._handleCherrypickConfirm({
-          detail: {
-            branch: '',
-            type: CHERRY_PICK_TYPES.SINGLE_CHANGE,
-          },
-        });
-        assert.equal(fireActionStub.callCount, 0);
-
-        element.$.confirmCherrypick.branch = 'master';
-        element._handleCherrypickConfirm({
-          detail: {
-            branch: 'master',
-            type: CHERRY_PICK_TYPES.SINGLE_CHANGE,
-          },
-        });
-        assert.equal(fireActionStub.callCount, 0); // Still needs a message.
-
-        // Add attributes that are used to determine the message.
-        element.$.confirmCherrypick.commitMessage = 'foo message';
-        element.$.confirmCherrypick.changeStatus = 'OPEN';
-        element.$.confirmCherrypick.commitNum = '123';
-
-        element._handleCherrypickConfirm({
-          detail: {
-            branch: 'master',
-            type: CHERRY_PICK_TYPES.SINGLE_CHANGE,
-          },
-        });
-
-        assert.equal(element.$.confirmCherrypick.shadowRoot.
-            querySelector('#messageInput').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', () => {
-        element._handleCherrypickTap();
-        const action = {
-          __key: 'cherrypick',
-          __type: 'revision',
-          __primary: false,
-          enabled: true,
-          label: 'Cherry pick',
-          method: 'POST',
-          title: 'Cherry pick change to a different branch',
-        };
-
-        element.$.confirmCherrypick.branch = 'master';
-
-        // Add attributes that are used to determine the message.
-        element.$.confirmCherrypick.commitMessage = 'foo message';
-        element.$.confirmCherrypick.changeStatus = 'OPEN';
-        element.$.confirmCherrypick.commitNum = '123';
-
-        element._handleCherrypickConflictConfirm();
-
-        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 = '';
-        element.$.confirmCherrypick.branch = 'master';
-
-        element._handleCherrypickTap();
-        assert.equal(element.$.confirmCherrypick.branch, emptyBranchName);
-      });
-
-      suite('cherry pick topics', () => {
-        const changes = [
-          {
-            change_id: '12345678901234', topic: 'T', subject: 'random',
-            project: 'A', status: 'MERGED',
-          },
-          {
-            change_id: '23456', topic: 'T', subject: 'a'.repeat(100),
-            project: 'B', status: 'NEW',
-          },
-        ];
-        setup(done => {
-          stubRestApi('getChanges')
-              .returns(Promise.resolve(changes));
-          element._handleCherrypickTap();
-          flush(() => {
-            const radioButtons = element.$.confirmCherrypick.shadowRoot.
-                querySelectorAll(`input[name='cherryPickOptions']`);
-            assert.equal(radioButtons.length, 2);
-            MockInteractions.tap(radioButtons[1]);
-            flush(() => {
-              done();
-            });
-          });
-        });
-
-        test('cherry pick topic dialog is rendered', done => {
-          const dialog = element.$.confirmCherrypick;
-          flush(() => {
-            const changesTable = dialog.shadowRoot.querySelector('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 = changesTable.querySelectorAll('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]);
-            }
-            done();
-          });
-        });
-
-        test('changes with duplicate project show an error', done => {
-          const dialog = element.$.confirmCherrypick;
-          const error = dialog.shadowRoot.querySelector('.error-message');
-          assert.equal(error.innerText, '');
-          dialog.updateChanges([
-            {
-              change_id: '12345678901234', topic: 'T', subject: 'random',
-              project: 'A',
-            },
-            {
-              change_id: '23456', topic: 'T', subject: 'a'.repeat(100),
-              project: 'A',
-            },
-          ]);
-          flush(() => {
-            assert.equal(error.innerText, 'Two changes cannot be of the same'
-             + ' project');
-            done();
-          });
-        });
-      });
-    });
-
-    suite('move change', () => {
-      let fireActionStub;
-
-      setup(() => {
-        fireActionStub = sinon.stub(element, '_fireAction');
-        sinon.stub(window, 'alert');
-        element.actions = {
-          move: {
-            method: 'POST',
-            label: 'Move',
-            title: 'Move the change',
-            enabled: true,
-          },
-        };
-      });
-
-      test('works', () => {
-        element._handleMoveTap();
-
-        element._handleMoveConfirm();
-        assert.equal(fireActionStub.callCount, 0);
-
-        element.$.confirmMove.branch = 'master';
-        element._handleMoveConfirm();
-        assert.equal(fireActionStub.callCount, 1);
-      });
-
-      test('branch name cleared when re-open move', () => {
-        const emptyBranchName = '';
-        element.$.confirmMove.branch = 'master';
-
-        element._handleMoveTap();
-        assert.equal(element.$.confirmMove.branch, emptyBranchName);
-      });
-    });
-
-    test('custom actions', done => {
-      // 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!');
-      element.addEventListener(key + '-tap', e => {
-        assert.equal(e.detail.node.getAttribute('data-action-key'), key);
-        element.removeActionButton(key);
-        flush(() => {
-          assert.notOk(element.shadowRoot
-              .querySelector('[data-action-key="' + key + '"]'));
-          done();
-        });
-      });
-      flush(() => {
-        MockInteractions.tap(element.shadowRoot
-            .querySelector('[data-action-key="' + key + '"]'));
-      });
-    });
-
-    test('_setLoadingOnButtonWithKey top-level', () => {
-      const key = 'rebase';
-      const type = 'revision';
-      const cleanup = element._setLoadingOnButtonWithKey(type, key);
-      assert.equal(element._actionLoadingMessage, 'Rebasing...');
-
-      const button = element.shadowRoot
-          .querySelector('[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;
-      let fireActionStub;
-
-      setup(() => {
-        fireActionStub = sinon.stub(element, '_fireAction');
-        alertStub = sinon.stub(window, 'alert');
-        element.actions = {
-          abandon: {
-            method: 'POST',
-            label: 'Abandon',
-            title: 'Abandon the change',
-            enabled: true,
-          },
-        };
-        return element.reload();
-      });
-
-      test('abandon change with message', done => {
-        const newAbandonMsg = 'Test Abandon Message';
-        element.$.confirmAbandonDialog.message = newAbandonMsg;
-        flush(() => {
-          const abandonButton =
-              element.shadowRoot
-                  .querySelector('gr-button[data-action-key="abandon"]');
-          MockInteractions.tap(abandonButton);
-
-          assert.equal(element.$.confirmAbandonDialog.message, newAbandonMsg);
-          done();
-        });
-      });
-
-      test('abandon change with no message', done => {
-        flush(() => {
-          const abandonButton =
-              element.shadowRoot
-                  .querySelector('gr-button[data-action-key="abandon"]');
-          MockInteractions.tap(abandonButton);
-
-          assert.isUndefined(element.$.confirmAbandonDialog.message);
-          done();
-        });
-      });
-
-      test('works', () => {
-        element.$.confirmAbandonDialog.message = 'original message';
-        const restoreButton =
-            element.shadowRoot
-                .querySelector('gr-button[data-action-key="abandon"]');
-        MockInteractions.tap(restoreButton);
-
-        element.$.confirmAbandonDialog.message = 'foo message';
-        element._handleAbandonDialogConfirm();
-        assert.notOk(alertStub.called);
-
-        const action = {
-          __key: 'abandon',
-          __type: 'change',
-          __primary: false,
-          enabled: true,
-          label: 'Abandon',
-          method: 'POST',
-          title: 'Abandon the change',
-        };
-        assert.deepEqual(fireActionStub.lastCall.args, [
-          '/abandon', action, false, {
-            message: 'foo message',
-          }]);
-      });
-    });
-
-    suite('revert change', () => {
-      let fireActionStub;
-
-      setup(() => {
-        fireActionStub = sinon.stub(element, '_fireAction');
-        element.commitMessage = 'random commit message';
-        element.change.current_revision = 'abcdef';
-        element.actions = {
-          revert: {
-            method: 'POST',
-            label: 'Revert',
-            title: 'Revert the change',
-            enabled: true,
-          },
-        };
-        return element.reload();
-      });
-
-      test('revert change with plugin hook', done => {
-        const newRevertMsg = 'Modified revert msg';
-        sinon.stub(element.$.confirmRevertDialog, '_modifyRevertMsg').callsFake(
-            () => newRevertMsg);
-        element.change = {
-          current_revision: 'abc1234',
-        };
-        stubRestApi('getChanges')
-            .returns(Promise.resolve([
-              {change_id: '12345678901234', topic: 'T', subject: 'random'},
-              {change_id: '23456', topic: 'T', subject: 'a'.repeat(100)},
-            ]));
-        sinon.stub(element.$.confirmRevertDialog,
-            '_populateRevertSubmissionMessage').callsFake(() => 'original msg');
-        flush(() => {
-          const revertButton = element.shadowRoot
-              .querySelector('gr-button[data-action-key="revert"]');
-          MockInteractions.tap(revertButton);
-          flush(() => {
-            assert.equal(element.$.confirmRevertDialog._message, newRevertMsg);
-            done();
-          });
-        });
-      });
-
-      suite('revert change submitted together', () => {
-        let getChangesStub;
-        setup(() => {
-          element.change = {
-            submission_id: '199 0',
-            current_revision: '2000',
-          };
-          getChangesStub = stubRestApi('getChanges')
-              .returns(Promise.resolve([
-                {change_id: '12345678901234', topic: 'T', subject: 'random'},
-                {change_id: '23456', topic: 'T', subject: 'a'.repeat(100)},
-              ]));
-        });
-
-        test('confirm revert dialog shows both options', done => {
-          const revertButton = element.shadowRoot
-              .querySelector('gr-button[data-action-key="revert"]');
-          MockInteractions.tap(revertButton);
-          flush(() => {
-            assert.equal(getChangesStub.args[0][1], 'submissionid: "199 0"');
-            const confirmRevertDialog = element.$.confirmRevertDialog;
-            const revertSingleChangeLabel = confirmRevertDialog
-                .shadowRoot.querySelector('.revertSingleChange');
-            const revertSubmissionLabel = confirmRevertDialog.
-                shadowRoot.querySelector('.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 = confirmRevertDialog.shadowRoot
-                .querySelectorAll('input[name="revertOptions"]');
-            MockInteractions.tap(radioInputs[0]);
-            flush(() => {
-              expectedMsg = 'Revert "random commit message"\n\nThis reverts '
-               + 'commit 2000.\n\nReason'
-               + ' for revert: <INSERT REASONING HERE>\n';
-              assert.equal(confirmRevertDialog._message, expectedMsg);
-              done();
-            });
-          });
-        });
-
-        test('submit fails if message is not edited', done => {
-          const revertButton = element.shadowRoot
-              .querySelector('gr-button[data-action-key="revert"]');
-          const confirmRevertDialog = element.$.confirmRevertDialog;
-          MockInteractions.tap(revertButton);
-          const fireStub = sinon.stub(confirmRevertDialog, 'dispatchEvent');
-          flush(() => {
-            const confirmButton = element.$.confirmRevertDialog.shadowRoot
-                .querySelector('gr-dialog')
-                .shadowRoot.querySelector('#confirm');
-            MockInteractions.tap(confirmButton);
-            flush(() => {
-              assert.isTrue(confirmRevertDialog._showErrorMessage);
-              assert.isFalse(fireStub.called);
-              done();
-            });
-          });
-        });
-
-        test('message modification is retained on switching', done => {
-          const revertButton = element.shadowRoot
-              .querySelector('gr-button[data-action-key="revert"]');
-          const confirmRevertDialog = element.$.confirmRevertDialog;
-          MockInteractions.tap(revertButton);
-          flush(() => {
-            const radioInputs = confirmRevertDialog.shadowRoot
-                .querySelectorAll('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;
-            MockInteractions.tap(radioInputs[0]);
-            flush(() => {
-              assert.equal(confirmRevertDialog._message, singleChangeMsg);
-              confirmRevertDialog._message = newSingleChangeMsg;
-              MockInteractions.tap(radioInputs[1]);
-              flush(() => {
-                assert.equal(confirmRevertDialog._message, newRevertMsg);
-                MockInteractions.tap(radioInputs[0]);
-                flush(() => {
-                  assert.equal(
-                      confirmRevertDialog._message,
-                      newSingleChangeMsg
-                  );
-                  done();
-                });
-              });
-            });
-          });
-        });
-      });
-
-      suite('revert single change', () => {
-        setup(() => {
-          element.change = {
-            submission_id: '199',
-            current_revision: '2000',
-          };
-          stubRestApi('getChanges')
-              .returns(Promise.resolve([
-                {change_id: '12345678901234', topic: 'T', subject: 'random'},
-              ]));
-        });
-
-        test('submit fails if message is not edited', done => {
-          const revertButton = element.shadowRoot
-              .querySelector('gr-button[data-action-key="revert"]');
-          const confirmRevertDialog = element.$.confirmRevertDialog;
-          MockInteractions.tap(revertButton);
-          const fireStub = sinon.stub(confirmRevertDialog, 'dispatchEvent');
-          flush(() => {
-            const confirmButton = element.$.confirmRevertDialog.shadowRoot
-                .querySelector('gr-dialog')
-                .shadowRoot.querySelector('#confirm');
-            MockInteractions.tap(confirmButton);
-            flush(() => {
-              assert.isTrue(confirmRevertDialog._showErrorMessage);
-              assert.isFalse(fireStub.called);
-              done();
-            });
-          });
-        });
-
-        test('confirm revert dialog shows no radio button', done => {
-          const revertButton = element.shadowRoot
-              .querySelector('gr-button[data-action-key="revert"]');
-          MockInteractions.tap(revertButton);
-          flush(() => {
-            const confirmRevertDialog = element.$.confirmRevertDialog;
-            const radioInputs = confirmRevertDialog.shadowRoot
-                .querySelectorAll('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);
-            const editedMsg = msg + 'hello';
-            confirmRevertDialog._message += 'hello';
-            const confirmButton = element.$.confirmRevertDialog.shadowRoot
-                .querySelector('gr-dialog')
-                .shadowRoot.querySelector('#confirm');
-            MockInteractions.tap(confirmButton);
-            flush(() => {
-              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);
-              done();
-            });
-          });
-        });
-      });
-    });
-
-    suite('mark change private', () => {
-      setup(() => {
-        const privateAction = {
-          __key: 'private',
-          __type: 'change',
-          __primary: false,
-          method: 'POST',
-          label: 'Mark private',
-          title: 'Working...',
-          enabled: true,
-        };
-
-        element.actions = {
-          private: privateAction,
-        };
-
-        element.change.is_private = false;
-
-        element.changeNum = '2';
-        element.latestPatchNum = '2';
-
-        return element.reload();
-      });
-
-      test('make sure the mark private change button is not outside of the ' +
-           'overflow menu', done => {
-        flush(() => {
-          assert.isNotOk(element.shadowRoot
-              .querySelector('[data-action-key="private"]'));
-          done();
-        });
-      });
-
-      test('private change', done => {
-        flush(() => {
-          assert.isOk(
-              element.$.moreActions.shadowRoot
-                  .querySelector('span[data-id="private-change"]'));
-          element.setActionOverflow('change', 'private', false);
-          flush();
-          assert.isOk(element.shadowRoot
-              .querySelector('[data-action-key="private"]'));
-          assert.isNotOk(
-              element.$.moreActions.shadowRoot
-                  .querySelector('span[data-id="private-change"]'));
-          done();
-        });
-      });
-    });
-
-    suite('unmark private change', () => {
-      setup(() => {
-        const unmarkPrivateAction = {
-          __key: 'private.delete',
-          __type: 'change',
-          __primary: false,
-          method: 'POST',
-          label: 'Unmark private',
-          title: 'Working...',
-          enabled: true,
-        };
-
-        element.actions = {
-          'private.delete': unmarkPrivateAction,
-        };
-
-        element.change.is_private = true;
-
-        element.changeNum = '2';
-        element.latestPatchNum = '2';
-
-        return element.reload();
-      });
-
-      test('make sure the unmark private change button is not outside of the ' +
-           'overflow menu', done => {
-        flush(() => {
-          assert.isNotOk(element.shadowRoot
-              .querySelector('[data-action-key="private.delete"]'));
-          done();
-        });
-      });
-
-      test('unmark the private change', done => {
-        flush(() => {
-          assert.isOk(
-              element.$.moreActions.shadowRoot
-                  .querySelector('span[data-id="private.delete-change"]')
-          );
-          element.setActionOverflow('change', 'private.delete', false);
-          flush();
-          assert.isOk(element.shadowRoot
-              .querySelector('[data-action-key="private.delete"]'));
-          assert.isNotOk(
-              element.$.moreActions.shadowRoot
-                  .querySelector('span[data-id="private.delete-change"]')
-          );
-          done();
-        });
-      });
-    });
-
-    suite('delete change', () => {
-      let fireActionStub;
-      let deleteAction;
-
-      setup(() => {
-        fireActionStub = sinon.stub(element, '_fireAction');
-        element.change = {
-          current_revision: 'abc1234',
-        };
-        deleteAction = {
-          method: 'DELETE',
-          label: 'Delete Change',
-          title: 'Delete change X_X',
-          enabled: true,
-        };
-        element.actions = {
-          '/': deleteAction,
-        };
-      });
-
-      test('does not delete on action', () => {
-        element._handleDeleteTap();
-        assert.isFalse(fireActionStub.called);
-      });
-
-      test('shows confirm dialog', () => {
-        element._handleDeleteTap();
-        assert.isFalse(element.shadowRoot
-            .querySelector('#confirmDeleteDialog').hidden);
-        MockInteractions.tap(
-            element.shadowRoot
-                .querySelector('#confirmDeleteDialog')
-                .shadowRoot
-                .querySelector('gr-button[primary]'));
-        flush();
-        assert.isTrue(fireActionStub.calledWith('/', deleteAction, false));
-      });
-
-      test('hides delete confirm on cancel', () => {
-        element._handleDeleteTap();
-        MockInteractions.tap(
-            element.shadowRoot
-                .querySelector('#confirmDeleteDialog')
-                .shadowRoot
-                .querySelector('gr-button:not([primary])'));
-        flush();
-        assert.isTrue(element.shadowRoot
-            .querySelector('#confirmDeleteDialog').hidden);
-        assert.isFalse(fireActionStub.called);
-      });
-    });
-
-    suite('ignore change', () => {
-      setup(done => {
-        sinon.stub(element, '_fireAction');
-
-        const IgnoreAction = {
-          __key: 'ignore',
-          __type: 'change',
-          __primary: false,
-          method: 'PUT',
-          label: 'Ignore',
-          title: 'Working...',
-          enabled: true,
-        };
-
-        element.actions = {
-          ignore: IgnoreAction,
-        };
-
-        element.changeNum = '2';
-        element.latestPatchNum = '2';
-
-        element.reload().then(() => { flush(done); });
-      });
-
-      test('make sure the ignore button is not outside of the overflow menu',
-          () => {
-            assert.isNotOk(element.shadowRoot
-                .querySelector('[data-action-key="ignore"]'));
-          });
-
-      test('ignoring change', () => {
-        assert.isOk(element.$.moreActions.shadowRoot
-            .querySelector('span[data-id="ignore-change"]'));
-        element.setActionOverflow('change', 'ignore', false);
-        flush();
-        assert.isOk(element.shadowRoot
-            .querySelector('[data-action-key="ignore"]'));
-        assert.isNotOk(
-            element.$.moreActions.shadowRoot
-                .querySelector('span[data-id="ignore-change"]'));
-      });
-    });
-
-    suite('unignore change', () => {
-      setup(done => {
-        sinon.stub(element, '_fireAction');
-
-        const UnignoreAction = {
-          __key: 'unignore',
-          __type: 'change',
-          __primary: false,
-          method: 'PUT',
-          label: 'Unignore',
-          title: 'Working...',
-          enabled: true,
-        };
-
-        element.actions = {
-          unignore: UnignoreAction,
-        };
-
-        element.changeNum = '2';
-        element.latestPatchNum = '2';
-
-        element.reload().then(() => { flush(done); });
-      });
-
-      test('unignore button is not outside of the overflow menu', () => {
-        assert.isNotOk(element.shadowRoot
-            .querySelector('[data-action-key="unignore"]'));
-      });
-
-      test('unignoring change', () => {
-        assert.isOk(
-            element.$.moreActions.shadowRoot
-                .querySelector('span[data-id="unignore-change"]'));
-        element.setActionOverflow('change', 'unignore', false);
-        flush();
-        assert.isOk(element.shadowRoot
-            .querySelector('[data-action-key="unignore"]'));
-        assert.isNotOk(
-            element.$.moreActions.shadowRoot
-                .querySelector('span[data-id="unignore-change"]'));
-      });
-    });
-
-    suite('reviewed change', () => {
-      setup(done => {
-        sinon.stub(element, '_fireAction');
-
-        const ReviewedAction = {
-          __key: 'reviewed',
-          __type: 'change',
-          __primary: false,
-          method: 'PUT',
-          label: 'Mark reviewed',
-          title: 'Working...',
-          enabled: true,
-        };
-
-        element.actions = {
-          reviewed: ReviewedAction,
-        };
-
-        element.changeNum = '2';
-        element.latestPatchNum = '2';
-
-        element.reload().then(() => { flush(done); });
-      });
-
-      test('action is enabled', () => {
-        assert.equal(element._allActionValues.filter(action =>
-          action.__key === 'reviewed').length, 1);
-      });
-
-      test('action is skipped when attention set is enabled', () => {
-        element._config = {
-          change: {enable_attention_set: true},
-        };
-        assert.equal(element._allActionValues.filter(action =>
-          action.__key === 'reviewed').length, 0);
-      });
-
-      test('make sure the reviewed button is not outside of the overflow menu',
-          () => {
-            assert.isNotOk(element.shadowRoot
-                .querySelector('[data-action-key="reviewed"]'));
-          });
-
-      test('reviewing change', () => {
-        assert.isOk(
-            element.$.moreActions.shadowRoot
-                .querySelector('span[data-id="reviewed-change"]'));
-        element.setActionOverflow('change', 'reviewed', false);
-        flush();
-        assert.isOk(element.shadowRoot
-            .querySelector('[data-action-key="reviewed"]'));
-        assert.isNotOk(
-            element.$.moreActions.shadowRoot
-                .querySelector('span[data-id="reviewed-change"]'));
-      });
-    });
-
-    suite('unreviewed change', () => {
-      setup(done => {
-        sinon.stub(element, '_fireAction');
-
-        const UnreviewedAction = {
-          __key: 'unreviewed',
-          __type: 'change',
-          __primary: false,
-          method: 'PUT',
-          label: 'Mark unreviewed',
-          title: 'Working...',
-          enabled: true,
-        };
-
-        element.actions = {
-          unreviewed: UnreviewedAction,
-        };
-
-        element.changeNum = '2';
-        element.latestPatchNum = '2';
-
-        element.reload().then(() => { flush(done); });
-      });
-
-      test('unreviewed button not outside of the overflow menu', () => {
-        assert.isNotOk(element.shadowRoot
-            .querySelector('[data-action-key="unreviewed"]'));
-      });
-
-      test('unreviewed change', () => {
-        assert.isOk(
-            element.$.moreActions.shadowRoot
-                .querySelector('span[data-id="unreviewed-change"]'));
-        element.setActionOverflow('change', 'unreviewed', false);
-        flush();
-        assert.isOk(element.shadowRoot
-            .querySelector('[data-action-key="unreviewed"]'));
-        assert.isNotOk(
-            element.$.moreActions.shadowRoot
-                .querySelector('span[data-id="unreviewed-change"]'));
-      });
-    });
-
-    suite('quick approve', () => {
-      setup(() => {
-        element.change = {
-          current_revision: 'abc1234',
-          labels: {
-            foo: {
-              values: {
-                '-1': '',
-                ' 0': '',
-                '+1': '',
-              },
-            },
-          },
-          permitted_labels: {
-            foo: ['-1', ' 0', '+1'],
-          },
-        };
-        flush();
-      });
-
-      test('added when can approve', () => {
-        const approveButton =
-            element.shadowRoot
-                .querySelector('gr-button[data-action-key=\'review\']');
-        assert.isNotNull(approveButton);
-      });
-
-      test('hide quick approve', () => {
-        const approveButton =
-            element.shadowRoot
-                .querySelector('gr-button[data-action-key=\'review\']');
-        assert.isNotNull(approveButton);
-        assert.isFalse(element._hideQuickApproveAction);
-
-        // Assert approve button gets removed from list of buttons.
-        element.hideQuickApproveAction();
-        flush();
-        const approveButtonUpdated =
-            element.shadowRoot
-                .querySelector('gr-button[data-action-key=\'review\']');
-        assert.isNull(approveButtonUpdated);
-        assert.isTrue(element._hideQuickApproveAction);
-      });
-
-      test('is first in list of secondary actions', () => {
-        const approveButton = element.$.secondaryActions
-            .querySelector('gr-button');
-        assert.equal(approveButton.getAttribute('data-label'), 'foo+1');
-      });
-
-      test('not added when change is merged', () => {
-        element.change.status = ChangeStatus.MERGED;
-        flush(() => {
-          const approveButton =
-          element.shadowRoot
-              .querySelector('gr-button[data-action-key=\'review\']');
-          assert.isNull(approveButton);
-        });
-      });
-
-      test('not added when already approved', () => {
-        element.change = {
-          current_revision: 'abc1234',
-          labels: {
-            foo: {
-              approved: {},
-              values: {},
-            },
-          },
-          permitted_labels: {
-            foo: [' 0', '+1'],
-          },
-        };
-        flush();
-        const approveButton =
-            element.shadowRoot
-                .querySelector('gr-button[data-action-key=\'review\']');
-        assert.isNull(approveButton);
-      });
-
-      test('not added when label not permitted', () => {
-        element.change = {
-          current_revision: 'abc1234',
-          labels: {
-            foo: {values: {}},
-          },
-          permitted_labels: {
-            bar: [],
-          },
-        };
-        flush();
-        const approveButton =
-            element.shadowRoot
-                .querySelector('gr-button[data-action-key=\'review\']');
-        assert.isNull(approveButton);
-      });
-
-      test('approves when tapped', () => {
-        const fireActionStub = sinon.stub(element, '_fireAction');
-        MockInteractions.tap(
-            element.shadowRoot
-                .querySelector('gr-button[data-action-key=\'review\']'));
-        flush();
-        assert.isTrue(fireActionStub.called);
-        assert.isTrue(fireActionStub.calledWith('/review'));
-        const payload = fireActionStub.lastCall.args[3];
-        assert.deepEqual(payload.labels, {foo: 1});
-      });
-
-      test('not added when multiple labels are required without code review',
-          () => {
-            element.change = {
-              current_revision: 'abc1234',
-              labels: {
-                foo: {values: {}},
-                bar: {values: {}},
-              },
-              permitted_labels: {
-                foo: [' 0', '+1'],
-                bar: [' 0', '+1', '+2'],
-              },
-            };
-            flush();
-            const approveButton =
-            element.shadowRoot
-                .querySelector('gr-button[data-action-key=\'review\']');
-            assert.isNull(approveButton);
-          });
-
-      test('code review shown with multiple missing approval', () => {
-        element.change = {
-          current_revision: 'abc1234',
-          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'],
-          },
-        };
-        flush();
-        const approveButton =
-            element.shadowRoot
-                .querySelector('gr-button[data-action-key=\'review\']');
-        assert.isOk(approveButton);
-      });
-
-      test('button label for missing approval', () => {
-        element.change = {
-          current_revision: 'abc1234',
-          labels: {
-            foo: {
-              values: {
-                ' 0': '',
-                '+1': '',
-              },
-            },
-            bar: {approved: {}, values: {}},
-          },
-          permitted_labels: {
-            foo: [' 0', '+1'],
-            bar: [' 0', '+1', '+2'],
-          },
-        };
-        flush();
-        const approveButton =
-            element.shadowRoot
-                .querySelector('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', () => {
-        element.change = {
-          current_revision: 'abc1234',
-          labels: {
-            bar: {
-              value: 1,
-              values: {
-                ' 0': '',
-                '+1': '',
-                '+2': '',
-              },
-            },
-          },
-          permitted_labels: {
-            bar: [' 0', '+1'],
-          },
-        };
-        flush();
-        const approveButton =
-            element.shadowRoot
-                .querySelector('gr-button[data-action-key=\'review\']');
-        assert.isNull(approveButton);
-      });
-
-      test('approving label with a non-max score', () => {
-        element.change = {
-          current_revision: 'abc1234',
-          labels: {
-            bar: {
-              value: 1,
-              values: {
-                ' 0': '',
-                '+1': '',
-                '+2': '',
-              },
-            },
-          },
-          permitted_labels: {
-            bar: [' 0', '+1', '+2'],
-          },
-        };
-        flush();
-        const approveButton =
-            element.shadowRoot
-                .querySelector('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',
-          () => {
-            element.change = {
-              current_revision: 'abc1234',
-              labels: {
-                'Code-Review': {
-                  approved: {},
-                  values: {
-                    ' 0': '',
-                    '+1': '',
-                    '+2': '',
-                  },
-                },
-              },
-              permitted_labels: {
-                'Code-Review': [' 0', '+1', '+2'],
-              },
-            };
-            flush();
-            const approveButton =
-                element.shadowRoot
-                    .querySelector('gr-button[data-action-key=\'review\']');
-            assert.isNotNull(approveButton);
-          });
-
-      test('not added when the user has already approved', () => {
-        const vote = {
-          ...createApproval(),
-          _account_id: 123,
-          name: 'name',
-          value: 2,
-        };
-        element.change = {
-          current_revision: 'abc1234',
-          labels: {
-            'Code-Review': {
-              approved: {},
-              values: {
-                ' 0': '',
-                '+1': '',
-                '+2': '',
-              },
-              all: [vote],
-            },
-          },
-          permitted_labels: {
-            'Code-Review': [' 0', '+1', '+2'],
-          },
-        };
-        flush();
-        const approveButton =
-            element.shadowRoot
-                .querySelector('gr-button[data-action-key=\'review\']');
-        assert.isNull(approveButton);
-      });
-
-      test('not added when user owns the change', () => {
-        element.change = {
-          current_revision: 'abc1234',
-          owner: createAccountWithId(123),
-          labels: {
-            'Code-Review': {
-              approved: {},
-              values: {
-                ' 0': '',
-                '+1': '',
-                '+2': '',
-              },
-            },
-          },
-          permitted_labels: {
-            'Code-Review': [' 0', '+1', '+2'],
-          },
-        };
-        flush();
-        const approveButton =
-            element.shadowRoot
-                .querySelector('gr-button[data-action-key=\'review\']');
-        assert.isNull(approveButton);
-      });
-    });
-
-    test('adds download revision action', () => {
-      const handler = sinon.stub();
-      element.addEventListener('download-tap', handler);
-      assert.ok(element.revisionActions.download);
-      element._handleDownloadTap();
-      flush();
-
-      assert.isTrue(handler.called);
-    });
-
-    test('changing changeNum or patchNum does not reload', () => {
-      const reloadStub = sinon.stub(element, 'reload');
-      element.changeNum = 123;
-      assert.isFalse(reloadStub.called);
-      element.latestPatchNum = 456;
-      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', () => {
-        assert.isNotOk(element.shadowRoot
-            .querySelector('[data-action-key="cherrypick"]'));
-        assert.strictEqual(
-            element.$.moreActions.items[0].id, 'cherrypick-revision');
-        element.setActionOverflow('revision', 'cherrypick', false);
-        flush();
-        assert.isOk(element.shadowRoot
-            .querySelector('[data-action-key="cherrypick"]'));
-        assert.notEqual(
-            element.$.moreActions.items[0].id, 'cherrypick-revision');
-      });
-
-      test('move action to overflow', () => {
-        assert.isOk(element.shadowRoot
-            .querySelector('[data-action-key="submit"]'));
-        element.setActionOverflow('revision', 'submit', true);
-        flush();
-        assert.isNotOk(element.shadowRoot
-            .querySelector('[data-action-key="submit"]'));
-        assert.strictEqual(
-            element.$.moreActions.items[3].id, 'submit-revision');
-      });
-
-      suite('_waitForChangeReachable', () => {
-        let clock;
-        setup(() => {
-          clock = sinon.useFakeTimers();
-        });
-
-        const makeGetChange = numTries => () => {
-          if (numTries === 1) {
-            return Promise.resolve({_number: 123});
-          } else {
-            numTries--;
-            return Promise.resolve(undefined);
-          }
-        };
-
-        const tickAndFlush = async repetitions => {
-          for (let i = 1; i <= repetitions; i++) {
-            clock.tick(1000);
-            await flush();
-          }
-        };
-
-        test('succeed', async () => {
-          stubRestApi('getChange').callsFake(makeGetChange(5));
-          const promise = element._waitForChangeReachable(123);
-          tickAndFlush(5);
-          const success = await promise;
-          assert.isTrue(success);
-        });
-
-        test('fail', async () => {
-          stubRestApi('getChange').callsFake(makeGetChange(6));
-          const promise = element._waitForChangeReachable(123);
-          tickAndFlush(6);
-          const success = await promise;
-          assert.isFalse(success);
-        });
-      });
-    });
-
-    suite('_send', () => {
-      let cleanup;
-      let payload;
-      let onShowError;
-      let onShowAlert;
-      let getResponseObjectStub;
-
-      setup(() => {
-        cleanup = sinon.stub();
-        element.changeNum = 42;
-        element.change._number = 42;
-        element.latestPatchNum = 12;
-        element.change = {
-          ...createChange(),
-          revisions: createRevisions(element.latestPatchNum),
-          messages: createChangeMessages(1),
-        };
-        payload = {foo: 'bar'};
-
-        onShowError = sinon.stub();
-        element.addEventListener('show-error', onShowError);
-        onShowAlert = sinon.stub();
-        element.addEventListener('show-alert', onShowAlert);
-      });
-
-      suite('happy path', () => {
-        let sendStub;
-        setup(() => {
-          stubRestApi('getChangeDetail')
-              .returns(Promise.resolve({
-                ...createChange(),
-                // element has latest info
-                revisions: createRevisions(element.latestPatchNum),
-                messages: createChangeMessages(1),
-              }));
-          sendStub = stubRestApi('executeChangeAction')
-              .returns(Promise.resolve({}));
-          getResponseObjectStub = stubRestApi(
-              'getResponseObject');
-          sinon.stub(GerritNav,
-              'navigateToChange').returns(Promise.resolve(true));
-        });
-
-        test('change action', async () => {
-          await element._send('DELETE', payload, '/endpoint', false, cleanup);
-          assert.isFalse(onShowError.called);
-          assert.isTrue(cleanup.calledOnce);
-          assert.isTrue(sendStub.calledWith(42, 'DELETE', '/endpoint',
-              undefined, payload));
-        });
-
-        suite('show revert submission dialog', () => {
-          setup(() => {
-            element.change.submission_id = '199';
-            element.change.current_revision = '2000';
-            stubRestApi('getChanges')
-                .returns(Promise.resolve([
-                  {change_id: '12345678901234', topic: 'T', subject: 'random'},
-                  {change_id: '23456', topic: 'T', subject: 'a'.repeat(100)},
-                ]));
-          });
-
-          test('revert submission shows submissionId', done => {
-            const expectedMsg = 'Revert submission 199' + '\n\n' +
-              'Reason for revert: <INSERT REASONING HERE>' + '\n' +
-              'Reverted Changes:' + '\n' +
-              '1234567890: random' + '\n' +
-              '23456: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...' +
-              '\n';
-            const modifiedMsg = expectedMsg + 'abcd';
-            sinon.stub(element.$.confirmRevertSubmissionDialog,
-                '_modifyRevertSubmissionMsg').returns(modifiedMsg);
-            element.showRevertSubmissionDialog();
-            flush(() => {
-              const msg = element.$.confirmRevertSubmissionDialog.message;
-              assert.equal(msg, modifiedMsg);
-              done();
-            });
-          });
-        });
-
-        suite('single changes revert', () => {
-          let navigateToSearchQueryStub;
-          setup(() => {
-            getResponseObjectStub
-                .returns(Promise.resolve({revert_changes: [
-                  {change_id: 12345},
-                ]}));
-            navigateToSearchQueryStub = sinon.stub(GerritNav,
-                'navigateToSearchQuery');
-          });
-
-          test('revert submission single change', done => {
-            element._send('POST', {message: 'Revert submission'},
-                '/revert_submission', false, cleanup).then(res => {
-              element._handleResponse({__key: 'revert_submission'}, {}).
-                  then(() => {
-                    assert.isTrue(navigateToSearchQueryStub.called);
-                    done();
-                  });
-            });
-          });
-        });
-
-        suite('multiple changes revert', () => {
-          let showActionDialogStub;
-          let navigateToSearchQueryStub;
-          setup(() => {
-            getResponseObjectStub
-                .returns(Promise.resolve({revert_changes: [
-                  {change_id: 12345, topic: 'T'},
-                  {change_id: 23456, topic: 'T'},
-                ]}));
-            showActionDialogStub = sinon.stub(element, '_showActionDialog');
-            navigateToSearchQueryStub = sinon.stub(GerritNav,
-                'navigateToSearchQuery');
-          });
-
-          test('revert submission multiple change', done => {
-            element._send('POST', {message: 'Revert submission'},
-                '/revert_submission', false, cleanup).then(res => {
-              element._handleResponse({__key: 'revert_submission'}, {}).then(
-                  () => {
-                    assert.isFalse(showActionDialogStub.called);
-                    assert.isTrue(navigateToSearchQueryStub.calledWith(
-                        'topic: T'));
-                    done();
-                  });
-            });
-          });
-        });
-
-        test('revision action', done => {
-          element
-              ._send('DELETE', payload, '/endpoint', true, cleanup)
-              .then(() => {
-                assert.isFalse(onShowError.called);
-                assert.isTrue(cleanup.calledOnce);
-                assert.isTrue(sendStub.calledWith(42, 'DELETE', '/endpoint',
-                    12, payload));
-                done();
-              });
-        });
-      });
-
-      suite('failure modes', () => {
-        test('non-latest', () => {
-          stubRestApi('getChangeDetail')
-              .returns(Promise.resolve({
-                ...createChange(),
-                // new patchset was uploaded
-                revisions: createRevisions(element.latestPatchNum + 1),
-                messages: createChangeMessages(1),
-              }));
-          const sendStub = stubRestApi(
-              'executeChangeAction');
-
-          return element._send('DELETE', payload, '/endpoint', true, cleanup)
-              .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({
-                ...createChange(),
-                // element has latest info
-                revisions: createRevisions(element.latestPatchNum),
-                messages: createChangeMessages(1),
-              }));
-          const sendStub = stubRestApi(
-              'executeChangeAction').callsFake(
-              (num, method, patchNum, endpoint, payload, onErr) => {
-                onErr();
-                return Promise.resolve(null);
-              });
-          const handleErrorStub = sinon.stub(element, '_handleResponseError');
-
-          return element._send('DELETE', payload, '/endpoint', true, cleanup)
-              .then(() => {
-                assert.isFalse(onShowError.called);
-                assert.isTrue(cleanup.called);
-                assert.isTrue(sendStub.calledOnce);
-                assert.isTrue(handleErrorStub.called);
-              });
-        });
-      });
-    });
-
-    test('_handleAction reports', () => {
-      sinon.stub(element, '_fireAction');
-      element.actions = {
-        key: {
-          __key: 'key',
-          __type: 'type',
-        },
-      };
-
-      const reportStub = sinon.stub(element.reporting, 'reportInteraction');
-      element._handleAction('type', 'key');
-      assert.isTrue(reportStub.called);
-      assert.equal(reportStub.lastCall.args[0], 'type-key');
-    });
-  });
-
-  suite('getChangeRevisionActions returns only some actions', () => {
-    let element;
-
-    let changeRevisionActions;
-
-    setup(() => {
-      stubRestApi('getChangeRevisionActions').returns(
-          Promise.resolve(changeRevisionActions));
-      stubRestApi('send').returns(Promise.reject(new Error('error')));
-      stubRestApi('getProjectConfig').returns(Promise.resolve({}));
-
-      sinon.stub(getPluginLoader(), 'awaitPluginsLoaded')
-          .returns(Promise.resolve());
-
-      element = basicFixture.instantiate();
-      // getChangeRevisionActions is not called without
-      // set the following properties
-      element.change = {};
-      element.changeNum = '42';
-      element.latestPatchNum = '2';
-
-      stubRestApi('getRepoBranches').returns(Promise.resolve([]));
-      return element.reload();
-    });
-
-    test('confirmSubmitDialog and confirmRebase properties are changed', () => {
-      changeRevisionActions = {};
-      element.reload();
-      assert.strictEqual(element.$.confirmSubmitDialog.action, null);
-      assert.strictEqual(element.$.confirmRebase.rebaseOnCurrent, null);
-    });
-
-    test('_computeRebaseOnCurrent', () => {
-      const rebaseAction = {
-        enabled: true,
-        label: 'Rebase',
-        method: 'POST',
-        title: 'Rebase onto tip of branch or parent change',
-      };
-
-      // When rebase is enabled initially, rebaseOnCurrent should be set to
-      // true.
-      assert.isTrue(element._computeRebaseOnCurrent(rebaseAction));
-
-      delete rebaseAction.enabled;
-
-      // When rebase is not enabled initially, rebaseOnCurrent should be set to
-      // false.
-      assert.isFalse(element._computeRebaseOnCurrent(rebaseAction));
-    });
-  });
-});
-
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts
new file mode 100644
index 0000000..6321e03
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts
@@ -0,0 +1,2600 @@
+/**
+ * @license
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '../../../test/common-test-setup-karma';
+import './gr-change-actions';
+import {GerritNav} from '../../core/gr-navigation/gr-navigation';
+import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
+import {
+  createAccountWithId,
+  createApproval,
+  createChange,
+  createChangeConfig,
+  createChangeMessages,
+  createChangeViewChange,
+  createRevision,
+  createRevisions,
+  createServerInfo,
+} from '../../../test/test-data-generators';
+import {ChangeStatus, HttpMethod} from '../../../constants/constants';
+import {
+  query,
+  queryAll,
+  queryAndAssert,
+  stubRestApi,
+} from '../../../test/test-utils';
+import {assertUIActionInfo, GrChangeActions} from './gr-change-actions';
+import {
+  AccountId,
+  ActionInfo,
+  ActionNameToActionInfoMap,
+  BranchName,
+  ChangeId,
+  ChangeSubmissionId,
+  CommitId,
+  NumericChangeId,
+  PatchSetNum,
+  RepoName,
+  ReviewInput,
+  TopicName,
+} from '../../../types/common';
+import {ActionType} from '../../../api/change-actions';
+import {tap} from '@polymer/iron-test-helpers/mock-interactions';
+import {SinonFakeTimers} from 'sinon/pkg/sinon-esm';
+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 {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces';
+import {appContext} from '../../../services/app-context';
+
+const basicFixture = fixtureFromElement('gr-change-actions');
+
+// TODO(dhruvsri): remove use of _populateRevertMessage as it's private
+suite('gr-change-actions tests', () => {
+  let element: GrChangeActions;
+
+  suite('basic tests', () => {
+    setup(() => {
+      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 = basicFixture.instantiate();
+      element.change = createChangeViewChange();
+      element.changeNum = 42 as NumericChangeId;
+      element.latestPatchNum = 2 as PatchSetNum;
+      element.actions = {
+        '/': {
+          method: HttpMethod.DELETE,
+          label: 'Delete Change',
+          title: 'Delete change X_X',
+          enabled: true,
+        },
+      };
+      element.account = {
+        _account_id: 123 as AccountId,
+      };
+      stubRestApi('getRepoBranches').returns(Promise.resolve([]));
+
+      return element.reload();
+    });
+
+    test('show-revision-actions event should fire', done => {
+      const spy = sinon.spy(element, '_sendShowRevisionActions');
+      element.reload();
+      flush(() => {
+        assert.isTrue(spy.called);
+        done();
+      });
+    });
+
+    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('_shouldHideActions', () => {
+      assert.isTrue(element._shouldHideActions(undefined, true));
+      assert.isTrue(
+        element._shouldHideActions(
+          {base: [] as UIActionInfo[]} as PolymerDeepPropertyChange<
+            UIActionInfo[],
+            UIActionInfo[]
+          >,
+          false
+        )
+      );
+      assert.isFalse(
+        element._shouldHideActions(
+          {
+            base: [{__key: 'test'}] as UIActionInfo[],
+          } as PolymerDeepPropertyChange<UIActionInfo[], UIActionInfo[]>,
+          false
+        )
+      );
+    });
+
+    test('plugin revision actions', done => {
+      const stub = stubRestApi('getChangeActionURL').returns(
+        Promise.resolve('the-url')
+      );
+      element.revisionActions = {
+        'plugin~action': {},
+      };
+      assert.isOk(element.revisionActions['plugin~action']);
+      flush(() => {
+        assert.isTrue(
+          stub.calledWith(
+            element.changeNum,
+            element.latestPatchNum,
+            '/plugin~action'
+          )
+        );
+        assert.equal(
+          (element.revisionActions['plugin~action'] as UIActionInfo)!.__url,
+          'the-url'
+        );
+        done();
+      });
+    });
+
+    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 flush();
+      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', done => {
+      flush(() => {
+        const buttonEl = queryAndAssert(element, '[data-action-key="submit"]');
+        assert.isOk(buttonEl);
+        element.setActionHidden(
+          element.ActionType.REVISION,
+          element.RevisionActions.SUBMIT,
+          true
+        );
+        assert.lengthOf(element._hiddenActions, 1);
+        element.setActionHidden(
+          element.ActionType.REVISION,
+          element.RevisionActions.SUBMIT,
+          true
+        );
+        assert.lengthOf(element._hiddenActions, 1);
+        flush(() => {
+          const buttonEl = element.shadowRoot?.querySelector(
+            '[data-action-key="submit"]'
+          );
+          assert.isNotOk(buttonEl);
+
+          element.setActionHidden(
+            element.ActionType.REVISION,
+            element.RevisionActions.SUBMIT,
+            false
+          );
+          flush(() => {
+            const buttonEl = queryAndAssert(
+              element,
+              '[data-action-key="submit"]'
+            );
+            assert.isFalse(buttonEl.hasAttribute('hidden'));
+            done();
+          });
+        });
+      });
+    });
+
+    test('buttons exist', done => {
+      element._loading = false;
+      flush(() => {
+        const buttonEls = queryAll(element, 'gr-button');
+        const menuItems = 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);
+        done();
+      });
+    });
+
+    test('delete buttons have explicit labels', done => {
+      flush(() => {
+        const deleteItems = element.$.moreActions.items!.filter(item =>
+          item.id!.startsWith('delete')
+        );
+        assert.equal(deleteItems.length, 1);
+        assert.equal(deleteItems[0].name, 'Delete change');
+        done();
+      });
+    });
+
+    test('get revision object from change', () => {
+      const revObj = {
+        ...createRevision(),
+        _number: 2 as PatchSetNum,
+        foo: 'bar',
+      };
+      const change = {
+        ...createChangeViewChange(),
+        revisions: {
+          rev1: {...createRevision(), _number: 1 as PatchSetNum},
+          rev2: revObj,
+        },
+      };
+      assert.deepEqual(element._getRevision(change, 2 as PatchSetNum), 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', () => {
+      const showSpy = sinon.spy(element, '_showActionDialog');
+      stubRestApi('getFromProjectLookup').returns(
+        Promise.resolve('test' as RepoName)
+      );
+      sinon.stub(element.$.overlay, 'open').returns(Promise.resolve());
+      element.change = {
+        ...createChangeViewChange(),
+        revisions: {
+          rev1: {...createRevision(), _number: 1 as PatchSetNum},
+          rev2: {...createRevision(), _number: 2 as PatchSetNum},
+        },
+      };
+      element.latestPatchNum = 2 as PatchSetNum;
+
+      const submitButton = queryAndAssert(
+        element,
+        'gr-button[data-action-key="submit"]'
+      );
+      tap(submitButton);
+
+      flush();
+      assert.isTrue(showSpy.calledWith(element.$.confirmSubmitDialog));
+    });
+
+    test('submit change, tap on icon', done => {
+      sinon.stub(element.$.confirmSubmitDialog, 'resetFocus').callsFake(done);
+      stubRestApi('getFromProjectLookup').returns(
+        Promise.resolve('test' as RepoName)
+      );
+      sinon.stub(element.$.overlay, 'open').returns(Promise.resolve());
+      element.change = {
+        ...createChangeViewChange(),
+        revisions: {
+          rev1: {...createRevision(), _number: 1 as PatchSetNum},
+          rev2: {...createRevision(), _number: 2 as PatchSetNum},
+        },
+      };
+      element.latestPatchNum = 2 as PatchSetNum;
+
+      const submitIcon = queryAndAssert(
+        element,
+        'gr-button[data-action-key="submit"] iron-icon'
+      );
+      tap(submitIcon);
+    });
+
+    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', done => {
+      sinon.stub(element, '_canSubmitChange').callsFake(() => false);
+      const fireActionStub = sinon.stub(element, '_fireAction');
+      flush(() => {
+        const submitButton = queryAndAssert(
+          element,
+          'gr-button[data-action-key="submit"]'
+        );
+        tap(submitButton);
+        assert.equal(fireActionStub.callCount, 0);
+
+        done();
+      });
+    });
+
+    test('chain state', () => {
+      assert.equal(element._hasKnownChainState, false);
+      element.hasParent = true;
+      assert.equal(element._hasKnownChainState, true);
+      element.hasParent = false;
+    });
+
+    test('_calculateDisabled', () => {
+      let hasKnownChainState = false;
+      const action = {
+        __key: 'rebase',
+        enabled: true,
+        __type: ActionType.CHANGE,
+        label: 'l',
+      };
+      assert.equal(
+        element._calculateDisabled(action, hasKnownChainState),
+        true
+      );
+
+      action.__key = 'delete';
+      assert.equal(
+        element._calculateDisabled(action, hasKnownChainState),
+        false
+      );
+
+      action.__key = 'rebase';
+      hasKnownChainState = true;
+      assert.equal(
+        element._calculateDisabled(action, hasKnownChainState),
+        false
+      );
+
+      action.enabled = false;
+      assert.equal(
+        element._calculateDisabled(action, hasKnownChainState),
+        false
+      );
+    });
+
+    test('rebase change', done => {
+      const fireActionStub = sinon.stub(element, '_fireAction');
+      const fetchChangesStub = sinon
+        .stub(element.$.confirmRebase, 'fetchRecentChanges')
+        .returns(Promise.resolve([]));
+      element._hasKnownChainState = true;
+      flush(() => {
+        const rebaseButton = queryAndAssert(
+          element,
+          'gr-button[data-action-key="rebase"]'
+        );
+        tap(rebaseButton);
+        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'}})
+        );
+        assert.deepEqual(fireActionStub.lastCall.args, [
+          '/rebase',
+          assertUIActionInfo(rebaseAction),
+          true,
+          {base: '1234'},
+        ]);
+        done();
+      });
+    });
+
+    test('rebase change fires reload event', done => {
+      const eventStub = sinon.stub(element, 'dispatchEvent');
+      element._handleResponse(
+        {__key: 'rebase', __type: ActionType.CHANGE, label: 'l'},
+        new Response()
+      );
+      flush(() => {
+        assert.isTrue(eventStub.called);
+        assert.equal(eventStub.lastCall.args[0].type, 'reload');
+        done();
+      });
+    });
+
+    test("rebase dialog gets recent changes each time it's opened", done => {
+      const fetchChangesStub = sinon
+        .stub(element.$.confirmRebase, 'fetchRecentChanges')
+        .returns(Promise.resolve([]));
+      element._hasKnownChainState = true;
+      const rebaseButton = queryAndAssert(
+        element,
+        'gr-button[data-action-key="rebase"]'
+      );
+      tap(rebaseButton);
+      assert.isTrue(fetchChangesStub.calledOnce);
+
+      flush(() => {
+        element.$.confirmRebase.dispatchEvent(
+          new CustomEvent('cancel', {
+            composed: true,
+            bubbles: true,
+          })
+        );
+        tap(rebaseButton);
+        assert.isTrue(fetchChangesStub.calledTwice);
+        done();
+      });
+    });
+
+    test('two dialogs are not shown at the same time', async () => {
+      element._hasKnownChainState = true;
+      await flush();
+      const rebaseButton = queryAndAssert(
+        element,
+        'gr-button[data-action-key="rebase"]'
+      );
+      tap(rebaseButton);
+      await flush();
+      assert.isFalse(element.$.confirmRebase.hidden);
+      stubRestApi('getChanges').returns(Promise.resolve([]));
+      element._handleCherrypickTap();
+      await flush();
+      assert.isTrue(element.$.confirmRebase.hidden);
+      assert.isFalse(element.$.confirmCherrypick.hidden);
+    });
+
+    test('fullscreen-overlay-opened hides content', () => {
+      const spy = sinon.spy(element, '_handleHideBackgroundContent');
+      element.$.overlay.dispatchEvent(
+        new CustomEvent('fullscreen-overlay-opened', {
+          composed: true,
+          bubbles: true,
+        })
+      );
+      assert.isTrue(spy.called);
+      assert.isTrue(element.$.mainContent.classList.contains('overlayOpen'));
+    });
+
+    test('fullscreen-overlay-closed shows content', () => {
+      const spy = sinon.spy(element, '_handleShowBackgroundContent');
+      element.$.overlay.dispatchEvent(
+        new CustomEvent('fullscreen-overlay-closed', {
+          composed: true,
+          bubbles: true,
+        })
+      );
+      assert.isTrue(spy.called);
+      assert.isFalse(element.$.mainContent.classList.contains('overlayOpen'));
+    });
+
+    test('_setReviewOnRevert', () => {
+      const review = {labels: {Foo: 1, 'Bar-Baz': -2}};
+      const changeId = 1234 as NumericChangeId;
+      sinon
+        .stub(appContext.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', () => {
+        element.set('editMode', false);
+        element.set('editPatchsetLoaded', false);
+        element.change = {
+          ...createChangeViewChange(),
+          status: ChangeStatus.NEW,
+        };
+        element.set('disableEdit', true);
+        flush();
+
+        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', () => {
+        element.set('editMode', true);
+        element.set('editPatchsetLoaded', true);
+
+        const fireActionStub = sinon.stub(element, '_fireAction');
+        element._handleDeleteEditTap();
+        assert.isFalse(element.$.confirmDeleteEditDialog.hidden);
+        tap(
+          queryAndAssert(
+            queryAndAssert(element, '#confirmDeleteEditDialog'),
+            'gr-button[primary]'
+          )
+        );
+        flush();
+
+        assert.equal(fireActionStub.lastCall.args[0], '/edit');
+      });
+
+      test('edit patchset is loaded, needs rebase', () => {
+        element.set('editMode', true);
+        element.set('editPatchsetLoaded', true);
+        element.change = {
+          ...createChangeViewChange(),
+          status: ChangeStatus.NEW,
+        };
+        element.editBasedOnCurrentPatchSet = false;
+        flush();
+
+        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', () => {
+        element.set('editMode', true);
+        element.set('editPatchsetLoaded', true);
+        element.change = {
+          ...createChangeViewChange(),
+          status: ChangeStatus.NEW,
+        };
+        element.editBasedOnCurrentPatchSet = true;
+        flush();
+
+        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', () => {
+        element.set('editMode', true);
+        element.set('editPatchsetLoaded', false);
+        element.change = {
+          ...createChangeViewChange(),
+          status: ChangeStatus.NEW,
+        };
+        flush();
+
+        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', () => {
+        element.set('editMode', false);
+        element.set('editPatchsetLoaded', false);
+        element.change = {
+          ...createChangeViewChange(),
+          status: ChangeStatus.NEW,
+        };
+        flush();
+
+        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', done => {
+        element.addEventListener('edit-tap', () => {
+          done();
+        });
+        element.set('editMode', true);
+        element.change = {
+          ...createChangeViewChange(),
+          status: ChangeStatus.NEW,
+        };
+        flush();
+
+        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,
+        };
+        flush();
+
+        assert.isNotOk(query(element, 'gr-button[data-action-key="edit"]'));
+        element.change = {
+          ...createChangeViewChange(),
+          status: ChangeStatus.NEW,
+        };
+        element.set('editMode', false);
+        flush();
+
+        const editButton = queryAndAssert(
+          element,
+          'gr-button[data-action-key="edit"]'
+        );
+        tap(editButton);
+      });
+    });
+
+    suite('cherry-pick', () => {
+      let fireActionStub: sinon.SinonStub;
+
+      setup(() => {
+        fireActionStub = sinon.stub(element, '_fireAction');
+        sinon.stub(window, 'alert');
+      });
+
+      test('works', () => {
+        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);
+
+        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.
+        element.$.confirmCherrypick.commitMessage = 'foo message';
+        element.$.confirmCherrypick.changeStatus = ChangeStatus.NEW;
+        element.$.confirmCherrypick.commitNum = '123' as CommitId;
+
+        element._handleCherrypickConfirm();
+
+        const autogrowEl = queryAndAssert(
+          element.$.confirmCherrypick,
+          '#messageInput'
+        ) as IronAutogrowTextareaElement;
+        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', () => {
+        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.$.confirmCherrypick.branch = 'master' as BranchName;
+
+        // Add attributes that are used to determine the message.
+        element.$.confirmCherrypick.commitMessage = 'foo message';
+        element.$.confirmCherrypick.changeStatus = ChangeStatus.NEW;
+        element.$.confirmCherrypick.commitNum = '123' as CommitId;
+
+        element._handleCherrypickConflictConfirm();
+
+        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 = '';
+        element.$.confirmCherrypick.branch = 'master' as BranchName;
+
+        element._handleCherrypickTap();
+        assert.equal(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(done => {
+          stubRestApi('getChanges').returns(Promise.resolve(changes));
+          element._handleCherrypickTap();
+          flush(() => {
+            const radioButtons = queryAll(
+              element.$.confirmCherrypick,
+              "input[name='cherryPickOptions']"
+            );
+            assert.equal(radioButtons.length, 2);
+            tap(radioButtons[1]);
+            flush(() => {
+              done();
+            });
+          });
+        });
+
+        test('cherry pick topic dialog is rendered', done => {
+          const dialog = element.$.confirmCherrypick;
+          flush(() => {
+            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]);
+            }
+            done();
+          });
+        });
+
+        test('changes with duplicate project show an error', done => {
+          const dialog = element.$.confirmCherrypick;
+          const error = queryAndAssert(
+            dialog,
+            '.error-message'
+          ) as HTMLSpanElement;
+          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,
+            },
+          ]);
+          flush(() => {
+            assert.equal(
+              error.innerText,
+              'Two changes cannot be of the same' + ' project'
+            );
+            done();
+          });
+        });
+      });
+    });
+
+    suite('move change', () => {
+      let fireActionStub: sinon.SinonStub;
+
+      setup(() => {
+        fireActionStub = sinon.stub(element, '_fireAction');
+        sinon.stub(window, 'alert');
+        element.actions = {
+          move: {
+            method: HttpMethod.POST,
+            label: 'Move',
+            title: 'Move the change',
+            enabled: true,
+          },
+        };
+      });
+
+      test('works', () => {
+        element._handleMoveTap();
+
+        element._handleMoveConfirm();
+        assert.equal(fireActionStub.callCount, 0);
+
+        element.$.confirmMove.branch = 'master' as BranchName;
+        element._handleMoveConfirm();
+        assert.equal(fireActionStub.callCount, 1);
+      });
+
+      test('branch name cleared when re-open move', () => {
+        const emptyBranchName = '';
+        element.$.confirmMove.branch = 'master' as BranchName;
+
+        element._handleMoveTap();
+        assert.equal(element.$.confirmMove.branch, emptyBranchName);
+      });
+    });
+
+    test('custom actions', done => {
+      // 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!');
+      element.addEventListener(key + '-tap', e => {
+        assert.equal(
+          (e as CustomEvent).detail.node.getAttribute('data-action-key'),
+          key
+        );
+        element.removeActionButton(key);
+        flush(() => {
+          assert.notOk(query(element, '[data-action-key="' + key + '"]'));
+          done();
+        });
+      });
+      flush(() => {
+        tap(queryAndAssert(element, '[data-action-key="' + key + '"]'));
+      });
+    });
+
+    test('_setLoadingOnButtonWithKey top-level', () => {
+      const key = 'rebase';
+      const type = 'revision';
+      const cleanup = element._setLoadingOnButtonWithKey(type, key);
+      assert.equal(element._actionLoadingMessage, 'Rebasing...');
+
+      const button = queryAndAssert(
+        element,
+        '[data-action-key="' + key + '"]'
+      ) as GrButton;
+      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(() => {
+        fireActionStub = sinon.stub(element, '_fireAction');
+        alertStub = sinon.stub(window, 'alert');
+        element.actions = {
+          abandon: {
+            method: HttpMethod.POST,
+            label: 'Abandon',
+            title: 'Abandon the change',
+            enabled: true,
+          },
+        };
+        return element.reload();
+      });
+
+      test('abandon change with message', done => {
+        const newAbandonMsg = 'Test Abandon Message';
+        element.$.confirmAbandonDialog.message = newAbandonMsg;
+        flush(() => {
+          const abandonButton = queryAndAssert(
+            element,
+            'gr-button[data-action-key="abandon"]'
+          );
+          tap(abandonButton);
+
+          assert.equal(element.$.confirmAbandonDialog.message, newAbandonMsg);
+          done();
+        });
+      });
+
+      test('abandon change with no message', done => {
+        flush(() => {
+          const abandonButton = queryAndAssert(
+            element,
+            'gr-button[data-action-key="abandon"]'
+          );
+          tap(abandonButton);
+
+          assert.isUndefined(element.$.confirmAbandonDialog.message);
+          done();
+        });
+      });
+
+      test('works', () => {
+        element.$.confirmAbandonDialog.message = 'original message';
+        const restoreButton = queryAndAssert(
+          element,
+          'gr-button[data-action-key="abandon"]'
+        );
+        tap(restoreButton);
+
+        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(() => {
+        fireActionStub = sinon.stub(element, '_fireAction');
+        element.commitMessage = 'random commit message';
+        element.change!.current_revision = 'abcdef' as CommitId;
+        element.actions = {
+          revert: {
+            method: HttpMethod.POST,
+            label: 'Revert',
+            title: 'Revert the change',
+            enabled: true,
+          },
+        };
+        return element.reload();
+      });
+
+      test('revert change with plugin hook', done => {
+        const newRevertMsg = 'Modified revert msg';
+        sinon
+          .stub(element.$.confirmRevertDialog, '_modifyRevertMsg')
+          .callsFake(() => newRevertMsg);
+        element.change = {
+          ...createChangeViewChange(),
+          current_revision: 'abc1234' as CommitId,
+        };
+        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(
+            element.$.confirmRevertDialog,
+            '_populateRevertSubmissionMessage'
+          )
+          .callsFake(() => 'original msg');
+        flush(() => {
+          const revertButton = queryAndAssert(
+            element,
+            'gr-button[data-action-key="revert"]'
+          );
+          tap(revertButton);
+          flush(() => {
+            assert.equal(element.$.confirmRevertDialog._message, newRevertMsg);
+            done();
+          });
+        });
+      });
+
+      suite('revert change submitted together', () => {
+        let getChangesStub: sinon.SinonStub;
+        setup(() => {
+          element.change = {
+            ...createChangeViewChange(),
+            submission_id: '199 0' as ChangeSubmissionId,
+            current_revision: '2000' as CommitId,
+          };
+          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),
+              },
+            ])
+          );
+        });
+
+        test('confirm revert dialog shows both options', done => {
+          const revertButton = queryAndAssert(
+            element,
+            'gr-button[data-action-key="revert"]'
+          );
+          tap(revertButton);
+          flush(() => {
+            assert.equal(getChangesStub.args[0][1], 'submissionid: "199 0"');
+            const confirmRevertDialog = element.$.confirmRevertDialog;
+            const revertSingleChangeLabel = queryAndAssert(
+              confirmRevertDialog,
+              '.revertSingleChange'
+            ) as HTMLLabelElement;
+            const revertSubmissionLabel = queryAndAssert(
+              confirmRevertDialog,
+              '.revertSubmission'
+            ) as HTMLLabelElement;
+            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(
+              confirmRevertDialog,
+              'input[name="revertOptions"]'
+            );
+            tap(radioInputs[0]);
+            flush(() => {
+              expectedMsg =
+                'Revert "random commit message"\n\nThis reverts ' +
+                'commit 2000.\n\nReason' +
+                ' for revert: <INSERT REASONING HERE>\n';
+              assert.equal(confirmRevertDialog._message, expectedMsg);
+              done();
+            });
+          });
+        });
+
+        test('submit fails if message is not edited', done => {
+          const revertButton = queryAndAssert(
+            element,
+            'gr-button[data-action-key="revert"]'
+          );
+          const confirmRevertDialog = element.$.confirmRevertDialog;
+          tap(revertButton);
+          const fireStub = sinon.stub(confirmRevertDialog, 'dispatchEvent');
+          flush(() => {
+            const confirmButton = queryAndAssert(
+              queryAndAssert(element.$.confirmRevertDialog, 'gr-dialog'),
+              '#confirm'
+            );
+            tap(confirmButton);
+            flush(() => {
+              assert.isTrue(confirmRevertDialog._showErrorMessage);
+              assert.isFalse(fireStub.called);
+              done();
+            });
+          });
+        });
+
+        test('message modification is retained on switching', done => {
+          const revertButton = queryAndAssert(
+            element,
+            'gr-button[data-action-key="revert"]'
+          );
+          const confirmRevertDialog = element.$.confirmRevertDialog;
+          tap(revertButton);
+          flush(() => {
+            const radioInputs = queryAll(
+              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;
+            tap(radioInputs[0]);
+            flush(() => {
+              assert.equal(confirmRevertDialog._message, singleChangeMsg);
+              confirmRevertDialog._message = newSingleChangeMsg;
+              tap(radioInputs[1]);
+              flush(() => {
+                assert.equal(confirmRevertDialog._message, newRevertMsg);
+                tap(radioInputs[0]);
+                flush(() => {
+                  assert.equal(
+                    confirmRevertDialog._message,
+                    newSingleChangeMsg
+                  );
+                  done();
+                });
+              });
+            });
+          });
+        });
+      });
+
+      suite('revert single change', () => {
+        setup(() => {
+          element.change = {
+            ...createChangeViewChange(),
+            submission_id: '199' as ChangeSubmissionId,
+            current_revision: '2000' as CommitId,
+          };
+          stubRestApi('getChanges').returns(
+            Promise.resolve([
+              {
+                ...createChange(),
+                change_id: '12345678901234' as ChangeId,
+                topic: 'T' as TopicName,
+                subject: 'random',
+              },
+            ])
+          );
+        });
+
+        test('submit fails if message is not edited', done => {
+          const revertButton = queryAndAssert(
+            element,
+            'gr-button[data-action-key="revert"]'
+          );
+          const confirmRevertDialog = element.$.confirmRevertDialog;
+          tap(revertButton);
+          const fireStub = sinon.stub(confirmRevertDialog, 'dispatchEvent');
+          flush(() => {
+            const confirmButton = queryAndAssert(
+              queryAndAssert(element.$.confirmRevertDialog, 'gr-dialog'),
+              '#confirm'
+            );
+            tap(confirmButton);
+            flush(() => {
+              assert.isTrue(confirmRevertDialog._showErrorMessage);
+              assert.isFalse(fireStub.called);
+              done();
+            });
+          });
+        });
+
+        test('confirm revert dialog shows no radio button', done => {
+          const revertButton = queryAndAssert(
+            element,
+            'gr-button[data-action-key="revert"]'
+          );
+          tap(revertButton);
+          flush(() => {
+            const confirmRevertDialog = 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);
+            const editedMsg = msg + 'hello';
+            confirmRevertDialog._message += 'hello';
+            const confirmButton = queryAndAssert(
+              queryAndAssert(element.$.confirmRevertDialog, 'gr-dialog'),
+              '#confirm'
+            );
+            tap(confirmButton);
+            flush(() => {
+              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
+              );
+              done();
+            });
+          });
+        });
+      });
+    });
+
+    suite('mark change private', () => {
+      setup(() => {
+        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;
+
+        return element.reload();
+      });
+
+      test(
+        'make sure the mark private change button is not outside of the ' +
+          'overflow menu',
+        done => {
+          flush(() => {
+            assert.isNotOk(query(element, '[data-action-key="private"]'));
+            done();
+          });
+        }
+      );
+
+      test('private change', done => {
+        flush(() => {
+          assert.isOk(
+            query(element.$.moreActions, 'span[data-id="private-change"]')
+          );
+          element.setActionOverflow(ActionType.CHANGE, 'private', false);
+          flush();
+          assert.isOk(query(element, '[data-action-key="private"]'));
+          assert.isNotOk(
+            query(element.$.moreActions, 'span[data-id="private-change"]')
+          );
+          done();
+        });
+      });
+    });
+
+    suite('unmark private change', () => {
+      setup(() => {
+        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;
+
+        return element.reload();
+      });
+
+      test(
+        'make sure the unmark private change button is not outside of the ' +
+          'overflow menu',
+        done => {
+          flush(() => {
+            assert.isNotOk(
+              query(element, '[data-action-key="private.delete"]')
+            );
+            done();
+          });
+        }
+      );
+
+      test('unmark the private change', done => {
+        flush(() => {
+          assert.isOk(
+            query(
+              element.$.moreActions,
+              'span[data-id="private.delete-change"]'
+            )
+          );
+          element.setActionOverflow(ActionType.CHANGE, 'private.delete', false);
+          flush();
+          assert.isOk(query(element, '[data-action-key="private.delete"]'));
+          assert.isNotOk(
+            query(
+              element.$.moreActions,
+              'span[data-id="private.delete-change"]'
+            )
+          );
+          done();
+        });
+      });
+    });
+
+    suite('delete change', () => {
+      let fireActionStub: sinon.SinonStub;
+      let deleteAction: ActionInfo;
+
+      setup(() => {
+        fireActionStub = sinon.stub(element, '_fireAction');
+        element.change = {
+          ...createChangeViewChange(),
+          current_revision: 'abc1234' as CommitId,
+        };
+        deleteAction = {
+          method: HttpMethod.DELETE,
+          label: 'Delete Change',
+          title: 'Delete change X_X',
+          enabled: true,
+        };
+        element.actions = {
+          '/': deleteAction,
+        };
+      });
+
+      test('does not delete on action', () => {
+        element._handleDeleteTap();
+        assert.isFalse(fireActionStub.called);
+      });
+
+      test('shows confirm dialog', () => {
+        element._handleDeleteTap();
+        assert.isFalse(
+          (queryAndAssert(element, '#confirmDeleteDialog') as GrDialog).hidden
+        );
+        tap(
+          queryAndAssert(
+            queryAndAssert(element, '#confirmDeleteDialog'),
+            'gr-button[primary]'
+          )
+        );
+        flush();
+        assert.isTrue(fireActionStub.calledWith('/', deleteAction, false));
+      });
+
+      test('hides delete confirm on cancel', () => {
+        element._handleDeleteTap();
+        tap(
+          queryAndAssert(
+            queryAndAssert(element, '#confirmDeleteDialog'),
+            'gr-button:not([primary])'
+          )
+        );
+        flush();
+        assert.isTrue(
+          (queryAndAssert(element, '#confirmDeleteDialog') as GrDialog).hidden
+        );
+        assert.isFalse(fireActionStub.called);
+      });
+    });
+
+    suite('ignore change', () => {
+      setup(done => {
+        sinon.stub(element, '_fireAction');
+
+        const IgnoreAction = {
+          __key: 'ignore',
+          __type: 'change',
+          __primary: false,
+          method: HttpMethod.PUT,
+          label: 'Ignore',
+          title: 'Working...',
+          enabled: true,
+        };
+
+        element.actions = {
+          ignore: IgnoreAction,
+        };
+
+        element.changeNum = 2 as NumericChangeId;
+        element.latestPatchNum = 2 as PatchSetNum;
+
+        element.reload().then(() => {
+          flush(done);
+        });
+      });
+
+      test('make sure the ignore button is not outside of the overflow menu', () => {
+        assert.isNotOk(query(element, '[data-action-key="ignore"]'));
+      });
+
+      test('ignoring change', () => {
+        queryAndAssert(element.$.moreActions, 'span[data-id="ignore-change"]');
+        element.setActionOverflow(ActionType.CHANGE, 'ignore', false);
+        flush();
+        queryAndAssert(element, '[data-action-key="ignore"]');
+        assert.isNotOk(
+          query(element.$.moreActions, 'span[data-id="ignore-change"]')
+        );
+      });
+    });
+
+    suite('unignore change', () => {
+      setup(done => {
+        sinon.stub(element, '_fireAction');
+
+        const UnignoreAction = {
+          __key: 'unignore',
+          __type: 'change',
+          __primary: false,
+          method: HttpMethod.PUT,
+          label: 'Unignore',
+          title: 'Working...',
+          enabled: true,
+        };
+
+        element.actions = {
+          unignore: UnignoreAction,
+        };
+
+        element.changeNum = 2 as NumericChangeId;
+        element.latestPatchNum = 2 as PatchSetNum;
+
+        element.reload().then(() => {
+          flush(done);
+        });
+      });
+
+      test('unignore button is not outside of the overflow menu', () => {
+        assert.isNotOk(query(element, '[data-action-key="unignore"]'));
+      });
+
+      test('unignoring change', () => {
+        assert.isOk(
+          query(element.$.moreActions, 'span[data-id="unignore-change"]')
+        );
+        element.setActionOverflow(ActionType.CHANGE, 'unignore', false);
+        flush();
+        assert.isOk(query(element, '[data-action-key="unignore"]'));
+        assert.isNotOk(
+          query(element.$.moreActions, 'span[data-id="unignore-change"]')
+        );
+      });
+    });
+
+    suite('reviewed change', () => {
+      setup(done => {
+        sinon.stub(element, '_fireAction');
+
+        const ReviewedAction = {
+          __key: 'reviewed',
+          __type: 'change',
+          __primary: false,
+          method: HttpMethod.PUT,
+          label: 'Mark reviewed',
+          title: 'Working...',
+          enabled: true,
+        };
+
+        element.actions = {
+          reviewed: ReviewedAction,
+        };
+
+        element.changeNum = 2 as NumericChangeId;
+        element.latestPatchNum = 2 as PatchSetNum;
+
+        element.reload().then(() => {
+          flush(done);
+        });
+      });
+
+      test('action is enabled', () => {
+        assert.equal(
+          element._allActionValues.filter(action => action.__key === 'reviewed')
+            .length,
+          1
+        );
+      });
+
+      test('action is skipped when attention set is enabled', () => {
+        element._config = {
+          ...createServerInfo(),
+          change: {...createChangeConfig(), enable_attention_set: true},
+        };
+        assert.equal(
+          element._allActionValues.filter(action => action.__key === 'reviewed')
+            .length,
+          0
+        );
+      });
+
+      test('make sure the reviewed button is not outside of the overflow menu', () => {
+        assert.isNotOk(query(element, '[data-action-key="reviewed"]'));
+      });
+
+      test('reviewing change', () => {
+        assert.isOk(
+          query(element.$.moreActions, 'span[data-id="reviewed-change"]')
+        );
+        element.setActionOverflow(ActionType.CHANGE, 'reviewed', false);
+        flush();
+        assert.isOk(query(element, '[data-action-key="reviewed"]'));
+        assert.isNotOk(
+          query(element.$.moreActions, 'span[data-id="reviewed-change"]')
+        );
+      });
+    });
+
+    suite('unreviewed change', () => {
+      setup(done => {
+        sinon.stub(element, '_fireAction');
+
+        const UnreviewedAction = {
+          __key: 'unreviewed',
+          __type: 'change',
+          __primary: false,
+          method: HttpMethod.PUT,
+          label: 'Mark unreviewed',
+          title: 'Working...',
+          enabled: true,
+        };
+
+        element.actions = {
+          unreviewed: UnreviewedAction,
+        };
+
+        element.changeNum = 2 as NumericChangeId;
+        element.latestPatchNum = 2 as PatchSetNum;
+
+        element.reload().then(() => {
+          flush(done);
+        });
+      });
+
+      test('unreviewed button not outside of the overflow menu', () => {
+        assert.isNotOk(query(element, '[data-action-key="unreviewed"]'));
+      });
+
+      test('unreviewed change', () => {
+        assert.isOk(
+          query(element.$.moreActions, 'span[data-id="unreviewed-change"]')
+        );
+        element.setActionOverflow(ActionType.CHANGE, 'unreviewed', false);
+        flush();
+        assert.isOk(query(element, '[data-action-key="unreviewed"]'));
+        assert.isNotOk(
+          query(element.$.moreActions, 'span[data-id="unreviewed-change"]')
+        );
+      });
+    });
+
+    suite('quick approve', () => {
+      setup(() => {
+        element.change = {
+          ...createChangeViewChange(),
+          current_revision: 'abc1234' as CommitId,
+          labels: {
+            foo: {
+              values: {
+                '-1': '',
+                ' 0': '',
+                '+1': '',
+              },
+            },
+          },
+          permitted_labels: {
+            foo: ['-1', ' 0', '+1'],
+          },
+        };
+        flush();
+      });
+
+      test('added when can approve', () => {
+        const approveButton = queryAndAssert(
+          element,
+          "gr-button[data-action-key='review']"
+        );
+        assert.isNotNull(approveButton);
+      });
+
+      test('hide quick approve', () => {
+        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();
+        flush();
+        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 = element.$.secondaryActions.querySelector(
+          'gr-button'
+        );
+        assert.equal(approveButton!.getAttribute('data-label'), 'foo+1');
+      });
+
+      test('not added when change is merged', () => {
+        element.change!.status = ChangeStatus.MERGED;
+        flush(() => {
+          const approveButton = query(
+            element,
+            "gr-button[data-action-key='review']"
+          );
+          assert.isNotOk(approveButton);
+        });
+      });
+
+      test('not added when already approved', () => {
+        element.change = {
+          ...createChangeViewChange(),
+          current_revision: 'abc1234' as CommitId,
+          labels: {
+            foo: {
+              approved: {},
+              values: {},
+            },
+          },
+          permitted_labels: {
+            foo: [' 0', '+1'],
+          },
+        };
+        flush();
+        const approveButton = query(
+          element,
+          "gr-button[data-action-key='review']"
+        );
+        assert.isNotOk(approveButton);
+      });
+
+      test('not added when label not permitted', () => {
+        element.change = {
+          ...createChangeViewChange(),
+          current_revision: 'abc1234' as CommitId,
+          labels: {
+            foo: {values: {}},
+          },
+          permitted_labels: {
+            bar: [],
+          },
+        };
+        flush();
+        const approveButton = query(
+          element,
+          "gr-button[data-action-key='review']"
+        );
+        assert.isNotOk(approveButton);
+      });
+
+      test('approves when tapped', () => {
+        const fireActionStub = sinon.stub(element, '_fireAction');
+        tap(queryAndAssert(element, "gr-button[data-action-key='review']"));
+        flush();
+        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', () => {
+        element.change = {
+          ...createChangeViewChange(),
+          current_revision: 'abc1234' as CommitId,
+          labels: {
+            foo: {values: {}},
+            bar: {values: {}},
+          },
+          permitted_labels: {
+            foo: [' 0', '+1'],
+            bar: [' 0', '+1', '+2'],
+          },
+        };
+        flush();
+        const approveButton = query(
+          element,
+          "gr-button[data-action-key='review']"
+        );
+        assert.isNotOk(approveButton);
+      });
+
+      test('code review shown with multiple missing approval', () => {
+        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'],
+          },
+        };
+        flush();
+        const approveButton = queryAndAssert(
+          element,
+          "gr-button[data-action-key='review']"
+        );
+        assert.isOk(approveButton);
+      });
+
+      test('button label for missing approval', () => {
+        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'],
+          },
+        };
+        flush();
+        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', () => {
+        element.change = {
+          ...createChangeViewChange(),
+          current_revision: 'abc1234' as CommitId,
+          labels: {
+            bar: {
+              value: 1,
+              values: {
+                ' 0': '',
+                '+1': '',
+                '+2': '',
+              },
+            },
+          },
+          permitted_labels: {
+            bar: [' 0', '+1'],
+          },
+        };
+        flush();
+        const approveButton = query(
+          element,
+          "gr-button[data-action-key='review']"
+        );
+        assert.isNotOk(approveButton);
+      });
+
+      test('approving label with a non-max score', () => {
+        element.change = {
+          ...createChangeViewChange(),
+          current_revision: 'abc1234' as CommitId,
+          labels: {
+            bar: {
+              value: 1,
+              values: {
+                ' 0': '',
+                '+1': '',
+                '+2': '',
+              },
+            },
+          },
+          permitted_labels: {
+            bar: [' 0', '+1', '+2'],
+          },
+        };
+        flush();
+        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', () => {
+        element.change = {
+          ...createChangeViewChange(),
+          current_revision: 'abc1234' as CommitId,
+          labels: {
+            'Code-Review': {
+              approved: {},
+              values: {
+                ' 0': '',
+                '+1': '',
+                '+2': '',
+              },
+            },
+          },
+          permitted_labels: {
+            'Code-Review': [' 0', '+1', '+2'],
+          },
+        };
+        flush();
+        const approveButton = queryAndAssert(
+          element,
+          "gr-button[data-action-key='review']"
+        );
+        assert.isNotNull(approveButton);
+      });
+
+      test('not added when the user has already approved', () => {
+        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'],
+          },
+        };
+        flush();
+        const approveButton = query(
+          element,
+          "gr-button[data-action-key='review']"
+        );
+        assert.isNotOk(approveButton);
+      });
+
+      test('not added when user owns the change', () => {
+        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'],
+          },
+        };
+        flush();
+        const approveButton = query(
+          element,
+          "gr-button[data-action-key='review']"
+        );
+        assert.isNotOk(approveButton);
+      });
+    });
+
+    test('adds download revision action', () => {
+      const handler = sinon.stub();
+      element.addEventListener('download-tap', handler);
+      assert.ok(element.revisionActions.download);
+      element._handleDownloadTap();
+      flush();
+
+      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', () => {
+        assert.isNotOk(query(element, '[data-action-key="cherrypick"]'));
+        assert.strictEqual(
+          element.$.moreActions!.items![0].id,
+          'cherrypick-revision'
+        );
+        element.setActionOverflow(ActionType.REVISION, 'cherrypick', false);
+        flush();
+        assert.isOk(query(element, '[data-action-key="cherrypick"]'));
+        assert.notEqual(
+          element.$.moreActions!.items![0].id,
+          'cherrypick-revision'
+        );
+      });
+
+      test('move action to overflow', () => {
+        assert.isOk(query(element, '[data-action-key="submit"]'));
+        element.setActionOverflow(ActionType.REVISION, 'submit', true);
+        flush();
+        assert.isNotOk(query(element, '[data-action-key="submit"]'));
+        assert.strictEqual(
+          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 flush();
+          }
+        };
+
+        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(() => {
+        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;
+
+        onShowError = sinon.stub();
+        element.addEventListener('show-error', onShowError);
+        onShowAlert = sinon.stub();
+        element.addEventListener('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())
+          );
+          sinon.stub(GerritNav, 'navigateToChange');
+        });
+
+        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(() => {
+            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),
+                },
+              ])
+            );
+          });
+
+          test('revert submission shows submissionId', done => {
+            const expectedMsg =
+              'Revert submission 199' +
+              '\n\n' +
+              'Reason for revert: <INSERT REASONING HERE>' +
+              '\n' +
+              'Reverted Changes:' +
+              '\n' +
+              '1234567890: random' +
+              '\n' +
+              '23456: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...' +
+              '\n';
+            const modifiedMsg = expectedMsg + 'abcd';
+            sinon
+              .stub(
+                element.$.confirmRevertSubmissionDialog,
+                '_modifyRevertSubmissionMsg'
+              )
+              .returns(modifiedMsg);
+            element.showRevertSubmissionDialog();
+            flush(() => {
+              const msg = element.$.confirmRevertSubmissionDialog.message;
+              assert.equal(msg, modifiedMsg);
+              done();
+            });
+          });
+        });
+
+        suite('single changes revert', () => {
+          let navigateToSearchQueryStub: sinon.SinonStub;
+          setup(() => {
+            getResponseObjectStub.returns(
+              Promise.resolve({revert_changes: [{change_id: 12345}]})
+            );
+            navigateToSearchQueryStub = sinon.stub(
+              GerritNav,
+              'navigateToSearchQuery'
+            );
+          });
+
+          test('revert submission single change', done => {
+            element
+              ._send(
+                HttpMethod.POST,
+                {message: 'Revert submission'},
+                '/revert_submission',
+                false,
+                cleanup,
+                {} as UIActionInfo
+              )
+              .then(() => {
+                element
+                  ._handleResponse(
+                    {
+                      __key: 'revert_submission',
+                      __type: ActionType.CHANGE,
+                      label: 'l',
+                    },
+                    new Response()
+                  )!
+                  .then(() => {
+                    assert.isTrue(navigateToSearchQueryStub.called);
+                    done();
+                  });
+              });
+          });
+        });
+
+        suite('multiple changes revert', () => {
+          let showActionDialogStub: sinon.SinonStub;
+          let navigateToSearchQueryStub: sinon.SinonStub;
+          setup(() => {
+            getResponseObjectStub.returns(
+              Promise.resolve({
+                revert_changes: [
+                  {change_id: 12345, topic: 'T'},
+                  {change_id: 23456, topic: 'T'},
+                ],
+              })
+            );
+            showActionDialogStub = sinon.stub(element, '_showActionDialog');
+            navigateToSearchQueryStub = sinon.stub(
+              GerritNav,
+              'navigateToSearchQuery'
+            );
+          });
+
+          test('revert submission multiple change', done => {
+            element
+              ._send(
+                HttpMethod.POST,
+                {message: 'Revert submission'},
+                '/revert_submission',
+                false,
+                cleanup,
+                {} as UIActionInfo
+              )
+              .then(() => {
+                element
+                  ._handleResponse(
+                    {
+                      __key: 'revert_submission',
+                      __type: ActionType.CHANGE,
+                      label: 'l',
+                    },
+                    new Response()
+                  )!
+                  .then(() => {
+                    assert.isFalse(showActionDialogStub.called);
+                    assert.isTrue(
+                      navigateToSearchQueryStub.calledWith('topic: T')
+                    );
+                    done();
+                  });
+              });
+          });
+        });
+
+        test('revision action', done => {
+          element
+            ._send(
+              HttpMethod.DELETE,
+              payload,
+              '/endpoint',
+              true,
+              cleanup,
+              {} as UIActionInfo
+            )
+            .then(() => {
+              assert.isFalse(onShowError.called);
+              assert.isTrue(cleanup.calledOnce);
+              assert.isTrue(
+                sendStub.calledWith(42, 'DELETE', '/endpoint', 12, payload)
+              );
+              done();
+            });
+        });
+      });
+
+      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) => {
+              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 = sinon.stub(element.reporting, '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(() => {
+      stubRestApi('getChangeRevisionActions').returns(
+        Promise.resolve(changeRevisionActions)
+      );
+      stubRestApi('send').returns(Promise.reject(new Error('error')));
+
+      sinon
+        .stub(getPluginLoader(), 'awaitPluginsLoaded')
+        .returns(Promise.resolve());
+
+      element = basicFixture.instantiate();
+      // 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([]));
+      return element.reload();
+    });
+
+    test('confirmSubmitDialog and confirmRebase properties are changed', () => {
+      changeRevisionActions = {};
+      element.reload();
+      assert.strictEqual(element.$.confirmSubmitDialog.action, null);
+      assert.strictEqual(element.$.confirmRebase.rebaseOnCurrent, null);
+    });
+
+    test('_computeRebaseOnCurrent', () => {
+      const rebaseAction = {
+        enabled: true,
+        label: 'Rebase',
+        method: HttpMethod.POST,
+        title: 'Rebase onto tip of branch or parent change',
+      };
+
+      // When rebase is enabled initially, rebaseOnCurrent should be set to
+      // true.
+      assert.isTrue(element._computeRebaseOnCurrent(rebaseAction));
+
+      rebaseAction.enabled = false;
+
+      // When rebase is not enabled initially, rebaseOnCurrent should be set to
+      // false.
+      assert.isFalse(element._computeRebaseOnCurrent(rebaseAction));
+    });
+  });
+});