blob: ec5c5a54856ffe93c42a2e60f4d20a1f1abc3ac0 [file] [log] [blame]
<!DOCTYPE html>
<!--
@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.
-->
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-change-actions</title>
<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../../test/common-test-setup.html"/>
<script src="../../../scripts/util.js"></script>
<link rel="import" href="gr-change-actions.html">
<script>void(0);</script>
<test-fixture id="basic">
<template>
<gr-change-actions></gr-change-actions>
</template>
</test-fixture>
<script>
suite('gr-change-actions tests', () => {
let element;
let sandbox;
setup(() => {
stub('gr-rest-api-interface', {
getChangeRevisionActions() {
return 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,
},
});
},
send(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'));
},
getProjectConfig() { return Promise.resolve({}); },
});
sandbox = sinon.sandbox.create();
sandbox.stub(Gerrit, 'awaitPluginsLoaded').returns(Promise.resolve());
element = fixture('basic');
element.change = {};
element.changeNum = '42';
element.latestPatchNum = '2';
element.actions = {
'/': {
method: 'DELETE',
label: 'Delete Change',
title: 'Delete change X_X',
enabled: true,
},
};
sandbox.stub(element.$.confirmCherrypick.$.restAPI,
'getRepoBranches').returns(Promise.resolve([]));
sandbox.stub(element.$.confirmMove.$.restAPI,
'getRepoBranches').returns(Promise.resolve([]));
return element.reload();
});
teardown(() => {
sandbox.restore();
});
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('_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 => {
sandbox.stub(element.$.restAPI, 'getChangeActionURL').returns(
Promise.resolve('the-url'));
element.revisionActions = {
'plugin~action': {},
};
assert.isOk(element.revisionActions['plugin~action']);
flush(() => {
assert.isTrue(element.$.restAPI.getChangeActionURL.calledWith(
element.changeNum, element.latestPatchNum, '/plugin~action'));
assert.equal(element.revisionActions['plugin~action'].__url, 'the-url');
done();
});
});
test('plugin change actions', done => {
sandbox.stub(element.$.restAPI, 'getChangeActionURL').returns(
Promise.resolve('the-url'));
element.actions = {
'plugin~action': {},
};
assert.isOk(element.actions['plugin~action']);
flush(() => {
assert.isTrue(element.$.restAPI.getChangeActionURL.calledWith(
element.changeNum, null, '/plugin~action'));
assert.equal(element.actions['plugin~action'].__url, 'the-url');
done();
});
});
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 = Object.assign({
'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.$$('[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.$$('[data-action-key="submit"]');
assert.isNotOk(buttonEl);
element.setActionHidden(element.ActionType.REVISION,
element.RevisionActions.SUBMIT, false);
flush(() => {
const buttonEl = element.$$('[data-action-key="submit"]');
assert.isOk(buttonEl);
assert.isFalse(buttonEl.hasAttribute('hidden'));
done();
});
});
});
});
test('buttons exist', done => {
element._loading = false;
flush(() => {
const buttonEls = Polymer.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 = sandbox.spy(element, '_showActionDialog');
sandbox.stub(element.$.restAPI, 'getFromProjectLookup')
.returns(Promise.resolve('test'));
sandbox.stub(element, 'fetchChangeUpdates',
() => Promise.resolve({isLatest: true}));
sandbox.stub(element.$.overlay, 'open').returns(Promise.resolve());
element.change = {
revisions: {
rev1: {_number: 1},
rev2: {_number: 2},
},
};
element.latestPatchNum = '2';
const submitButton = element.$$('gr-button[data-action-key="submit"]');
assert.ok(submitButton);
MockInteractions.tap(submitButton);
flushAsynchronousOperations();
assert.isTrue(showSpy.calledWith(element.$.confirmSubmitDialog));
});
test('submit change, tap on icon', done => {
sandbox.stub(element.$.confirmSubmitDialog, 'resetFocus', done);
sandbox.stub(element.$.restAPI, 'getFromProjectLookup')
.returns(Promise.resolve('test'));
sandbox.stub(element, 'fetchChangeUpdates',
() => Promise.resolve({isLatest: true}));
sandbox.stub(element.$.overlay, 'open').returns(Promise.resolve());
element.change = {
revisions: {
rev1: {_number: 1},
rev2: {_number: 2},
},
};
element.latestPatchNum = '2';
const submitIcon =
element.$$('gr-button[data-action-key="submit"] iron-icon');
assert.ok(submitIcon);
MockInteractions.tap(submitIcon);
});
test('_handleSubmitConfirm', () => {
const fireStub = sandbox.stub(element, '_fireAction');
sandbox.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 = sandbox.stub(element, '_fireAction');
sandbox.stub(element, '_canSubmitChange').returns(false);
element._handleSubmitConfirm();
assert.isFalse(fireStub.called);
});
test('submit change with plugin hook', done => {
sandbox.stub(element, '_canSubmitChange',
() => false);
const fireActionStub = sandbox.stub(element, '_fireAction');
flush(() => {
const submitButton = element.$$('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), true);
});
test('rebase change', done => {
const fireActionStub = sandbox.stub(element, '_fireAction');
const fetchChangesStub = sandbox.stub(element.$.confirmRebase,
'fetchRecentChanges').returns(Promise.resolve([]));
element._hasKnownChainState = true;
flush(() => {
const rebaseButton = element.$$('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'}});
rebaseAction.rebaseOnCurrent = true;
assert.deepEqual(fireActionStub.lastCall.args,
['/rebase', rebaseAction, true, {base: '1234'}]);
done();
});
});
test(`rebase dialog gets recent changes each time it's opened`, done => {
const fetchChangesStub = sandbox.stub(element.$.confirmRebase,
'fetchRecentChanges').returns(Promise.resolve([]));
element._hasKnownChainState = true;
const rebaseButton = element.$$('gr-button[data-action-key="rebase"]');
MockInteractions.tap(rebaseButton);
assert.isTrue(fetchChangesStub.calledOnce);
flush(() => {
element.$.confirmRebase.fire('cancel');
MockInteractions.tap(rebaseButton);
assert.isTrue(fetchChangesStub.calledTwice);
done();
});
});
test('two dialogs are not shown at the same time', done => {
element._hasKnownChainState = true;
flush(() => {
const rebaseButton = element.$$('gr-button[data-action-key="rebase"]');
assert.ok(rebaseButton);
MockInteractions.tap(rebaseButton);
flushAsynchronousOperations();
assert.isFalse(element.$.confirmRebase.hidden);
element._handleCherrypickTap();
flushAsynchronousOperations();
assert.isTrue(element.$.confirmRebase.hidden);
assert.isFalse(element.$.confirmCherrypick.hidden);
done();
});
});
test('fullscreen-overlay-opened hides content', () => {
sandbox.spy(element, '_handleHideBackgroundContent');
element.$.overlay.fire('fullscreen-overlay-opened');
assert.isTrue(element._handleHideBackgroundContent.called);
assert.isTrue(element.$.mainContent.classList.contains('overlayOpen'));
});
test('fullscreen-overlay-closed shows content', () => {
sandbox.spy(element, '_handleShowBackgroundContent');
element.$.overlay.fire('fullscreen-overlay-closed');
assert.isTrue(element._handleShowBackgroundContent.called);
assert.isFalse(element.$.mainContent.classList.contains('overlayOpen'));
});
test('_setLabelValuesOnRevert', () => {
const labels = {'Foo': 1, 'Bar-Baz': -2};
const changeId = 1234;
sandbox.stub(element.$.jsAPI, 'getLabelValuesPostRevert').returns(labels);
const saveStub = sandbox.stub(element.$.restAPI, 'saveChangeReview')
.returns(Promise.resolve());
return element._setLabelValuesOnRevert(changeId).then(() => {
assert.isTrue(saveStub.calledOnce);
assert.equal(saveStub.lastCall.args[0], changeId);
assert.deepEqual(saveStub.lastCall.args[2], {labels});
});
});
suite('change edits', () => {
test('disableEdit', () => {
element.set('editMode', false);
element.set('editPatchsetLoaded', false);
element.change = {status: 'NEW'};
element.set('disableEdit', true);
flushAsynchronousOperations();
assert.isNotOk(element.$$('gr-button[data-action-key="publishEdit"]'));
assert.isNotOk(element.$$('gr-button[data-action-key="rebaseEdit"]'));
assert.isNotOk(element.$$('gr-button[data-action-key="deleteEdit"]'));
assert.isNotOk(element.$$('gr-button[data-action-key="edit"]'));
assert.isNotOk(element.$$('gr-button[data-action-key="stopEdit"]'));
});
test('shows confirm dialog for delete edit', () => {
element.set('editMode', true);
element.set('editPatchsetLoaded', true);
const fireActionStub = sandbox.stub(element, '_fireAction');
element._handleDeleteEditTap();
assert.isFalse(element.$.confirmDeleteEditDialog.hidden);
MockInteractions.tap(
element.shadowRoot
.querySelector('#confirmDeleteEditDialog')
.$$('gr-button[primary]'));
flushAsynchronousOperations();
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'};
flushAsynchronousOperations();
assert.isNotOk(element.$$('gr-button[data-action-key="publishEdit"]'));
assert.isNotOk(element.$$('gr-button[data-action-key="rebaseEdit"]'));
assert.isOk(element.$$('gr-button[data-action-key="deleteEdit"]'));
assert.isNotOk(element.$$('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;
flushAsynchronousOperations();
assert.isNotOk(element.$$('gr-button[data-action-key="publishEdit"]'));
assert.isOk(element.$$('gr-button[data-action-key="rebaseEdit"]'));
assert.isOk(element.$$('gr-button[data-action-key="deleteEdit"]'));
assert.isNotOk(element.$$('gr-button[data-action-key="edit"]'));
assert.isNotOk(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 = {status: 'NEW'};
element.editBasedOnCurrentPatchSet = true;
flushAsynchronousOperations();
assert.isOk(element.$$('gr-button[data-action-key="publishEdit"]'));
assert.isNotOk(element.$$('gr-button[data-action-key="rebaseEdit"]'));
assert.isOk(element.$$('gr-button[data-action-key="deleteEdit"]'));
assert.isNotOk(element.$$('gr-button[data-action-key="edit"]'));
assert.isNotOk(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 = {status: 'NEW'};
flushAsynchronousOperations();
assert.isNotOk(element.$$('gr-button[data-action-key="publishEdit"]'));
assert.isNotOk(element.$$('gr-button[data-action-key="rebaseEdit"]'));
assert.isNotOk(element.$$('gr-button[data-action-key="deleteEdit"]'));
assert.isNotOk(element.$$('gr-button[data-action-key="edit"]'));
assert.isOk(element.$$('gr-button[data-action-key="stopEdit"]'));
});
test('normal patch set', () => {
element.set('editMode', false);
element.set('editPatchsetLoaded', false);
element.change = {status: 'NEW'};
flushAsynchronousOperations();
assert.isNotOk(element.$$('gr-button[data-action-key="publishEdit"]'));
assert.isNotOk(element.$$('gr-button[data-action-key="rebaseEdit"]'));
assert.isNotOk(element.$$('gr-button[data-action-key="deleteEdit"]'));
assert.isOk(element.$$('gr-button[data-action-key="edit"]'));
assert.isNotOk(element.$$('gr-button[data-action-key="stopEdit"]'));
});
test('edit action', done => {
element.addEventListener('edit-tap', () => { done(); });
element.set('editMode', true);
element.change = {status: 'NEW'};
flushAsynchronousOperations();
assert.isNotOk(element.$$('gr-button[data-action-key="edit"]'));
assert.isOk(element.$$('gr-button[data-action-key="stopEdit"]'));
element.change = {status: 'MERGED'};
flushAsynchronousOperations();
assert.isNotOk(element.$$('gr-button[data-action-key="edit"]'));
element.change = {status: 'NEW'};
element.set('editMode', false);
flushAsynchronousOperations();
const editButton = element.$$('gr-button[data-action-key="edit"]');
assert.isOk(editButton);
MockInteractions.tap(editButton);
});
});
suite('cherry-pick', () => {
let fireActionStub;
setup(() => {
fireActionStub = sandbox.stub(element, '_fireAction');
sandbox.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();
assert.equal(fireActionStub.callCount, 0);
element.$.confirmCherrypick.branch = 'master';
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 = 'OPEN';
element.$.confirmCherrypick.commitNum = '123';
element._handleCherrypickConfirm();
assert.equal(element.$.confirmCherrypick.$.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('move change', () => {
let fireActionStub;
setup(() => {
fireActionStub = sandbox.stub(element, '_fireAction');
sandbox.stub(window, 'alert');
});
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.$$('[data-action-key="' + key + '"]'));
done();
});
});
flush(() => {
MockInteractions.tap(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 = element.$$('[data-action-key="' + key + '"]');
assert.isTrue(button.hasAttribute('loading'));
assert.isTrue(button.disabled);
assert.isOk(cleanup);
assert.isFunction(cleanup);
cleanup();
assert.isFalse(button.hasAttribute('loading'));
assert.isFalse(button.disabled);
assert.isNotOk(element._actionLoadingMessage);
});
test('_setLoadingOnButtonWithKey overflow menu', () => {
const key = 'cherrypick';
const type = 'revision';
const cleanup = element._setLoadingOnButtonWithKey(type, key);
assert.equal(element._actionLoadingMessage, 'Cherry-picking...');
assert.include(element._disabledMenuActions, 'cherrypick');
assert.isFunction(cleanup);
cleanup();
assert.notOk(element._actionLoadingMessage);
assert.notInclude(element._disabledMenuActions, 'cherrypick');
});
suite('abandon change', () => {
let alertStub;
let fireActionStub;
setup(() => {
fireActionStub = sandbox.stub(element, '_fireAction');
alertStub = sandbox.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.$$('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.$$('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.$$('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 alertStub;
let fireActionStub;
setup(() => {
fireActionStub = sandbox.stub(element, '_fireAction');
alertStub = sandbox.stub(window, 'alert');
element.actions = {
revert: {
method: 'POST',
label: 'Revert',
title: 'Revert the change',
enabled: true,
},
};
return element.reload();
});
test('revert change with plugin hook', done => {
element.change = {
current_revision: 'abc1234',
};
const newRevertMsg = 'Modified revert msg';
sandbox.stub(element, '_modifyRevertMsg',
() => newRevertMsg);
sandbox.stub(element.$.confirmRevertDialog, 'populateRevertMessage',
() => 'original msg');
flush(() => {
const revertButton =
element.$$('gr-button[data-action-key="revert"]');
MockInteractions.tap(revertButton);
assert.equal(element.$.confirmRevertDialog.message, newRevertMsg);
done();
});
});
test('works', () => {
element.change = {
current_revision: 'abc1234',
};
sandbox.stub(element.$.confirmRevertDialog, 'populateRevertMessage',
() => 'original msg');
const revertButton = element.$$('gr-button[data-action-key="revert"]');
MockInteractions.tap(revertButton);
element.$.confirmRevertDialog.message = 'foo message';
element._handleRevertDialogConfirm();
assert.notOk(alertStub.called);
const action = {
__key: 'revert',
__type: 'change',
__primary: false,
enabled: true,
label: 'Revert',
method: 'POST',
title: 'Revert the change',
};
assert.deepEqual(fireActionStub.lastCall.args, [
'/revert', action, false, {
message: 'foo message',
}]);
});
});
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.$$('[data-action-key="private"]'));
done();
});
});
test('private change', done => {
flush(() => {
assert.isOk(
element.$.moreActions.$$('span[data-id="private-change"]'));
element.setActionOverflow('change', 'private', false);
flushAsynchronousOperations();
assert.isOk(element.$$('[data-action-key="private"]'));
assert.isNotOk(
element.$.moreActions.$$('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.$$('[data-action-key="private.delete"]'));
done();
});
});
test('unmark the private change', done => {
flush(() => {
assert.isOk(
element.$.moreActions.$$('span[data-id="private.delete-change"]')
);
element.setActionOverflow('change', 'private.delete', false);
flushAsynchronousOperations();
assert.isOk(element.$$('[data-action-key="private.delete"]'));
assert.isNotOk(
element.$.moreActions.$$('span[data-id="private.delete-change"]')
);
done();
});
});
});
suite('delete change', () => {
let fireActionStub;
let deleteAction;
setup(() => {
fireActionStub = sandbox.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')
.$$('gr-button[primary]'));
flushAsynchronousOperations();
assert.isTrue(fireActionStub.calledWith('/', deleteAction, false));
});
test('hides delete confirm on cancel', () => {
element._handleDeleteTap();
MockInteractions.tap(
element.shadowRoot
.querySelector('#confirmDeleteDialog')
.$$('gr-button:not([primary])'));
flushAsynchronousOperations();
assert.isTrue(element.shadowRoot
.querySelector('#confirmDeleteDialog').hidden);
assert.isFalse(fireActionStub.called);
});
});
suite('ignore change', () => {
setup(done => {
sandbox.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.$$('[data-action-key="ignore"]'));
});
test('ignoring change', () => {
assert.isOk(element.$.moreActions.$$('span[data-id="ignore-change"]'));
element.setActionOverflow('change', 'ignore', false);
flushAsynchronousOperations();
assert.isOk(element.$$('[data-action-key="ignore"]'));
assert.isNotOk(
element.$.moreActions.$$('span[data-id="ignore-change"]'));
});
});
suite('unignore change', () => {
setup(done => {
sandbox.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.$$('[data-action-key="unignore"]'));
});
test('unignoring change', () => {
assert.isOk(
element.$.moreActions.$$('span[data-id="unignore-change"]'));
element.setActionOverflow('change', 'unignore', false);
flushAsynchronousOperations();
assert.isOk(element.$$('[data-action-key="unignore"]'));
assert.isNotOk(
element.$.moreActions.$$('span[data-id="unignore-change"]'));
});
});
suite('reviewed change', () => {
setup(done => {
sandbox.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('make sure the reviewed button is not outside of the overflow menu',
() => {
assert.isNotOk(element.$$('[data-action-key="reviewed"]'));
});
test('reviewing change', () => {
assert.isOk(
element.$.moreActions.$$('span[data-id="reviewed-change"]'));
element.setActionOverflow('change', 'reviewed', false);
flushAsynchronousOperations();
assert.isOk(element.$$('[data-action-key="reviewed"]'));
assert.isNotOk(
element.$.moreActions.$$('span[data-id="reviewed-change"]'));
});
});
suite('unreviewed change', () => {
setup(done => {
sandbox.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.$$('[data-action-key="unreviewed"]'));
});
test('unreviewed change', () => {
assert.isOk(
element.$.moreActions.$$('span[data-id="unreviewed-change"]'));
element.setActionOverflow('change', 'unreviewed', false);
flushAsynchronousOperations();
assert.isOk(element.$$('[data-action-key="unreviewed"]'));
assert.isNotOk(
element.$.moreActions.$$('span[data-id="unreviewed-change"]'));
});
});
suite('quick approve', () => {
setup(() => {
element.change = {
current_revision: 'abc1234',
};
element.change = {
current_revision: 'abc1234',
labels: {
foo: {
values: {
'-1': '',
' 0': '',
'+1': '',
},
},
},
permitted_labels: {
foo: ['-1', ' 0', '+1'],
},
};
flushAsynchronousOperations();
});
test('added when can approve', () => {
const approveButton =
element.$$('gr-button[data-action-key=\'review\']');
assert.isNotNull(approveButton);
});
test('hide quick approve', () => {
const approveButton =
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();
flushAsynchronousOperations();
const approveButtonUpdated =
element.$$('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 already approved', () => {
element.change = {
current_revision: 'abc1234',
labels: {
foo: {
approved: {},
values: {},
},
},
permitted_labels: {
foo: [' 0', '+1'],
},
};
flushAsynchronousOperations();
const approveButton =
element.$$('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: [],
},
};
flushAsynchronousOperations();
const approveButton =
element.$$('gr-button[data-action-key=\'review\']');
assert.isNull(approveButton);
});
test('approves when tapped', () => {
const fireActionStub = sandbox.stub(element, '_fireAction');
MockInteractions.tap(
element.$$('gr-button[data-action-key=\'review\']'));
flushAsynchronousOperations();
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', () => {
element.change = {
current_revision: 'abc1234',
labels: {
foo: {values: {}},
bar: {values: {}},
},
permitted_labels: {
foo: [' 0', '+1'],
bar: [' 0', '+1', '+2'],
},
};
flushAsynchronousOperations();
const approveButton =
element.$$('gr-button[data-action-key=\'review\']');
assert.isNull(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'],
},
};
flushAsynchronousOperations();
const approveButton =
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 = {
current_revision: 'abc1234',
labels: {
bar: {
value: 1,
values: {
' 0': '',
'+1': '',
'+2': '',
},
},
},
permitted_labels: {
bar: [' 0', '+1'],
},
};
flushAsynchronousOperations();
const approveButton =
element.$$('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'],
},
};
flushAsynchronousOperations();
const approveButton =
element.$$('gr-button[data-action-key=\'review\']');
assert.equal(approveButton.getAttribute('data-label'), 'bar+2');
});
});
test('adds download revision action', () => {
const handler = sandbox.stub();
element.addEventListener('download-tap', handler);
assert.ok(element.revisionActions.download);
element._handleDownloadTap();
flushAsynchronousOperations();
assert.isTrue(handler.called);
});
test('changing changeNum or patchNum does not reload', () => {
const reloadStub = sandbox.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.$$('[data-action-key="cherrypick"]'));
assert.strictEqual(
element.$.moreActions.items[0].id, 'cherrypick-revision');
element.setActionOverflow('revision', 'cherrypick', false);
flushAsynchronousOperations();
assert.isOk(element.$$('[data-action-key="cherrypick"]'));
assert.notEqual(
element.$.moreActions.items[0].id, 'cherrypick-revision');
});
test('move action to overflow', () => {
assert.isOk(element.$$('[data-action-key="submit"]'));
element.setActionOverflow('revision', 'submit', true);
flushAsynchronousOperations();
assert.isNotOk(element.$$('[data-action-key="submit"]'));
assert.strictEqual(
element.$.moreActions.items[3].id, 'submit-revision');
});
suite('_waitForChangeReachable', () => {
setup(() => {
sandbox.stub(element, 'async', fn => fn());
});
const makeGetChange = numTries => () => {
if (numTries === 1) {
return Promise.resolve({_number: 123});
} else {
numTries--;
return Promise.resolve(undefined);
}
};
test('succeed', () => {
sandbox.stub(element.$.restAPI, 'getChange', makeGetChange(5));
return element._waitForChangeReachable(123).then(success => {
assert.isTrue(success);
});
});
test('fail', () => {
sandbox.stub(element.$.restAPI, 'getChange', makeGetChange(6));
return element._waitForChangeReachable(123).then(success => {
assert.isFalse(success);
});
});
});
});
suite('_send', () => {
let cleanup;
let payload;
let onShowError;
let onShowAlert;
let getResponseObjectStub;
setup(() => {
cleanup = sinon.stub();
element.changeNum = 42;
element.latestPatchNum = 12;
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(() => {
sandbox.stub(element, 'fetchChangeUpdates')
.returns(Promise.resolve({isLatest: true}));
sendStub = sandbox.stub(element.$.restAPI, 'executeChangeAction')
.returns(Promise.resolve({}));
getResponseObjectStub = sandbox.stub(element.$.restAPI,
'getResponseObject');
sandbox.stub(Gerrit.Nav,
'navigateToChange').returns(Promise.resolve(true));
});
test('change action', () => element
._send('DELETE', payload, '/endpoint', false, cleanup)
.then(() => {
assert.isFalse(onShowError.called);
assert.isTrue(cleanup.calledOnce);
assert.isTrue(sendStub.calledWith(42, 'DELETE', '/endpoint',
null, payload));
}));
suite('show revert submission dialog', () => {
setup(() => {
element.change.submission_id = '199';
element.change.current_revision = '2000';
sandbox.stub(element.$.restAPI, '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';
sandbox.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},
]}));
showActionDialogStub = sandbox.stub(element, '_showActionDialog');
navigateToSearchQueryStub = sandbox.stub(Gerrit.Nav,
'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 = sandbox.stub(element, '_showActionDialog');
navigateToSearchQueryStub = sandbox.stub(Gerrit.Nav,
'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', () => 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));
}));
});
suite('failure modes', () => {
test('non-latest', () => {
sandbox.stub(element, 'fetchChangeUpdates')
.returns(Promise.resolve({isLatest: false}));
const sendStub = sandbox.stub(element.$.restAPI,
'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', () => {
sandbox.stub(element, 'fetchChangeUpdates')
.returns(Promise.resolve({isLatest: true}));
const sendStub = sandbox.stub(element.$.restAPI,
'executeChangeAction',
(num, method, patchNum, endpoint, payload, onErr) => {
onErr();
return Promise.resolve(null);
});
const handleErrorStub = sandbox.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', () => {
sandbox.stub(element, '_fireAction');
const reportStub = sandbox.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 sandbox;
let changeRevisionActions;
setup(() => {
stub('gr-rest-api-interface', {
getChangeRevisionActions() {
return Promise.resolve(changeRevisionActions);
},
send(method, url, payload) {
return Promise.reject(new Error('error'));
},
getProjectConfig() { return Promise.resolve({}); },
});
sandbox = sinon.sandbox.create();
sandbox.stub(Gerrit, 'awaitPluginsLoaded').returns(Promise.resolve());
element = fixture('basic');
// getChangeRevisionActions is not called without
// set the following properies
element.change = {};
element.changeNum = '42';
element.latestPatchNum = '2';
sandbox.stub(element.$.confirmCherrypick.$.restAPI,
'getRepoBranches').returns(Promise.resolve([]));
sandbox.stub(element.$.confirmMove.$.restAPI,
'getRepoBranches').returns(Promise.resolve([]));
return element.reload();
});
teardown(() => {
sandbox.restore();
});
test('confirmSubmitDialog and confirmRebase properties are changed', () => {
changeRevisionActions = {};
element.reload();
assert.strictEqual(element.$.confirmSubmitDialog.action, null);
assert.strictEqual(element.$.confirmRebase.rebaseOnCurrent, null);
});
test('_updateRebaseAction sets _parentIsCurrent on no rebase', () => {
const currentRevisionActions = {
cherrypick: {
enabled: true,
label: 'Cherry Pick',
method: 'POST',
title: 'cherrypick',
},
};
element._parentIsCurrent = undefined;
element._updateRebaseAction(currentRevisionActions);
assert.isTrue(element._parentIsCurrent);
});
test('_updateRebaseAction', () => {
const currentRevisionActions = {
cherrypick: {
enabled: true,
label: 'Cherry Pick',
method: 'POST',
title: 'cherrypick',
},
rebase: {
enabled: true,
label: 'Rebase',
method: 'POST',
title: 'Rebase onto tip of branch or parent change',
},
};
element._parentIsCurrent = undefined;
// Rebase enabled should always end up true.
// When rebase is enabled initially, rebaseOnCurrent should be set to
// true.
assert.equal(element._updateRebaseAction(currentRevisionActions),
currentRevisionActions);
assert.isTrue(currentRevisionActions.rebase.enabled);
assert.isTrue(currentRevisionActions.rebase.rebaseOnCurrent);
assert.isFalse(element._parentIsCurrent);
delete currentRevisionActions.rebase.enabled;
// When rebase is not enabled initially, rebaseOnCurrent should be set to
// false.
assert.equal(element._updateRebaseAction(currentRevisionActions),
currentRevisionActions);
assert.isTrue(currentRevisionActions.rebase.enabled);
assert.isFalse(currentRevisionActions.rebase.rebaseOnCurrent);
assert.isTrue(element._parentIsCurrent);
});
});
</script>