blob: 1960ced6d2e99d7a075657c1a522ce0605348722 [file] [log] [blame]
/**
* @license
* Copyright (C) 2015 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-diff-view.js';
import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
import {ChangeStatus} from '../../../constants/constants.js';
import {TestKeyboardShortcutBinder, stubRestApi} from '../../../test/test-utils.js';
import {Shortcut} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js';
import {ChangeComments} from '../gr-comment-api/gr-comment-api.js';
import {GerritView} from '../../../services/router/router-model.js';
import {
createChange,
createRevisions,
createComment,
} from '../../../test/test-data-generators.js';
import {EditPatchSetNum} from '../../../types/common.js';
import sinon from 'sinon/pkg/sinon-esm';
import {CursorMoveResult} from '../../../api/core.js';
const basicFixture = fixtureFromElement('gr-diff-view');
const blankFixture = fixtureFromElement('div');
suite('gr-diff-view tests', () => {
suite('basic tests', () => {
let element;
let clock;
let diffCommentsStub;
suiteSetup(() => {
const kb = TestKeyboardShortcutBinder.push();
kb.bindShortcut(Shortcut.LEFT_PANE, 'shift+left');
kb.bindShortcut(Shortcut.RIGHT_PANE, 'shift+right');
kb.bindShortcut(Shortcut.NEXT_LINE, 'j', 'down');
kb.bindShortcut(Shortcut.PREV_LINE, 'k', 'up');
kb.bindShortcut(Shortcut.NEXT_FILE_WITH_COMMENTS, 'shift+j');
kb.bindShortcut(Shortcut.PREV_FILE_WITH_COMMENTS, 'shift+k');
kb.bindShortcut(Shortcut.NEW_COMMENT, 'c');
kb.bindShortcut(Shortcut.SAVE_COMMENT, 'ctrl+s');
kb.bindShortcut(Shortcut.NEXT_FILE, ']');
kb.bindShortcut(Shortcut.PREV_FILE, '[');
kb.bindShortcut(Shortcut.NEXT_CHUNK, 'n');
kb.bindShortcut(Shortcut.NEXT_COMMENT_THREAD, 'shift+n');
kb.bindShortcut(Shortcut.PREV_CHUNK, 'p');
kb.bindShortcut(Shortcut.PREV_COMMENT_THREAD, 'shift+p');
kb.bindShortcut(Shortcut.OPEN_REPLY_DIALOG, 'a');
kb.bindShortcut(Shortcut.OPEN_DOWNLOAD_DIALOG, 'd');
kb.bindShortcut(Shortcut.TOGGLE_LEFT_PANE, 'shift+a');
kb.bindShortcut(Shortcut.UP_TO_CHANGE, 'u');
kb.bindShortcut(Shortcut.OPEN_DIFF_PREFS, ',');
kb.bindShortcut(Shortcut.TOGGLE_DIFF_MODE, 'm');
kb.bindShortcut(Shortcut.TOGGLE_FILE_REVIEWED, 'r');
kb.bindShortcut(Shortcut.TOGGLE_ALL_DIFF_CONTEXT, 'shift+x');
kb.bindShortcut(Shortcut.EXPAND_ALL_COMMENT_THREADS, 'e');
kb.bindShortcut(Shortcut.TOGGLE_HIDE_ALL_COMMENT_THREADS, 'h');
kb.bindShortcut(Shortcut.COLLAPSE_ALL_COMMENT_THREADS, 'shift+e');
kb.bindShortcut(Shortcut.NEXT_UNREVIEWED_FILE, 'shift+m');
kb.bindShortcut(Shortcut.TOGGLE_BLAME, 'b');
kb.bindShortcut(Shortcut.OPEN_FILE_LIST, 'f');
});
suiteTeardown(() => {
TestKeyboardShortcutBinder.pop();
});
const PARENT = 'PARENT';
function getFilesFromFileList(fileList) {
const changeFilesByPath = fileList.reduce((files, path) => {
files[path] = {};
return files;
}, {});
return {
sortedFileList: fileList,
changeFilesByPath,
};
}
let getDiffChangeDetailStub;
setup(async () => {
clock = sinon.useFakeTimers();
stubRestApi('getConfig').returns(Promise.resolve({change: {}}));
stubRestApi('getLoggedIn').returns(Promise.resolve(false));
stubRestApi('getProjectConfig').returns(Promise.resolve({}));
getDiffChangeDetailStub = stubRestApi('getDiffChangeDetail').returns(
Promise.resolve({}));
stubRestApi('getChangeFiles').returns(Promise.resolve({}));
stubRestApi('saveFileReviewed').returns(Promise.resolve());
diffCommentsStub = stubRestApi('getDiffComments');
diffCommentsStub.returns(Promise.resolve({}));
stubRestApi('getDiffRobotComments').returns(Promise.resolve({}));
stubRestApi('getDiffDrafts').returns(Promise.resolve({}));
stubRestApi('getPortedComments').returns(Promise.resolve({}));
element = basicFixture.instantiate();
element._changeNum = '42';
element._path = 'some/path.txt';
element._change = {};
element._diff = {content: []};
element._patchRange = {
patchNum: 77,
basePatchNum: 'PARENT',
};
element._changeComments = new ChangeComments({'/COMMIT_MSG': [
{
...createComment(),
id: 'c1',
line: 10,
patch_set: 2,
path: '/COMMIT_MSG',
}, {
...createComment(),
id: 'c3',
line: 10,
patch_set: 'PARENT',
path: '/COMMIT_MSG',
},
]});
await flush();
});
teardown(() => {
clock.restore();
sinon.restore();
});
test('params change triggers diffViewDisplayed()', () => {
sinon.stub(element.reporting, 'diffViewDisplayed');
sinon.stub(element.$.diffHost, 'reload').returns(Promise.resolve());
sinon.stub(element, '_initPatchRange');
sinon.stub(element, '_getFiles');
sinon.spy(element, '_paramsChanged');
element.params = {
view: GerritNav.View.DIFF,
changeNum: '42',
patchNum: 2,
basePatchNum: 1,
path: '/COMMIT_MSG',
};
element._path = '/COMMIT_MSG';
element._patchRange = {};
return element._paramsChanged.returnValues[0].then(() => {
assert.isTrue(element.reporting.diffViewDisplayed.calledOnce);
});
});
suite('comment route', () => {
let initLineOfInterestAndCursorStub; let getUrlStub; let replaceStateStub;
setup(() => {
initLineOfInterestAndCursorStub =
sinon.stub(element, '_initLineOfInterestAndCursor');
getUrlStub = sinon.stub(GerritNav, 'getUrlForDiffById');
replaceStateStub = sinon.stub(history, 'replaceState');
sinon.stub(element, '_getFiles');
sinon.stub(element.reporting, 'diffViewDisplayed');
sinon.stub(element.$.diffHost, 'reload').returns(Promise.resolve());
sinon.spy(element, '_paramsChanged');
sinon.stub(element, '_getChangeDetail').returns(Promise.resolve({
...createChange(),
revisions: createRevisions(11),
}));
});
test('comment url resolves to comment.patch_set vs latest', () => {
diffCommentsStub.returns(Promise.resolve({
'/COMMIT_MSG': [
{
...createComment(),
id: 'c1',
line: 10,
patch_set: 2,
path: '/COMMIT_MSG',
}, {
...createComment(),
id: 'c3',
line: 10,
patch_set: 'PARENT',
path: '/COMMIT_MSG',
},
]}));
element.params = {
view: GerritNav.View.DIFF,
changeNum: '42',
commentLink: true,
commentId: 'c1',
};
element._change = {
...createChange(),
revisions: createRevisions(11),
};
return element._paramsChanged.returnValues[0].then(() => {
assert.isTrue(initLineOfInterestAndCursorStub.
calledWithExactly(true));
assert.equal(element._focusLineNum, 10);
assert.equal(element._patchRange.patchNum, 11);
assert.equal(element._patchRange.basePatchNum, 2);
assert.isTrue(replaceStateStub.called);
assert.isTrue(getUrlStub.calledWithExactly('42', 'test-project',
'/COMMIT_MSG', 11, 2, 10, true));
});
});
});
test('params change causes blame to load if it was set to true', () => {
// Blame loads for subsequent files if it was loaded for one file
element._isBlameLoaded = true;
sinon.stub(element.reporting, 'diffViewDisplayed');
sinon.stub(element, '_loadBlame');
sinon.stub(element.$.diffHost, 'reload').returns(Promise.resolve());
sinon.spy(element, '_paramsChanged');
sinon.stub(element, '_initPatchRange');
sinon.stub(element, '_getFiles');
element.params = {
view: GerritNav.View.DIFF,
changeNum: '42',
patchNum: 2,
basePatchNum: 1,
path: '/COMMIT_MSG',
};
element._path = '/COMMIT_MSG';
element._patchRange = {};
return element._paramsChanged.returnValues[0].then(() => {
assert.isTrue(element._isBlameLoaded);
assert.isTrue(element._loadBlame.calledOnce);
});
});
test('unchanged diff X vs latest from comment links navigates to base vs X'
, () => {
const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
diffCommentsStub.returns(Promise.resolve({
'/COMMIT_MSG': [
{
...createComment(),
id: 'c1',
line: 10,
patch_set: 2,
path: '/COMMIT_MSG',
}, {
...createComment(),
id: 'c3',
line: 10,
patch_set: 'PARENT',
path: '/COMMIT_MSG',
},
]}));
sinon.stub(element.reporting, 'diffViewDisplayed');
sinon.stub(element, '_loadBlame');
sinon.stub(element.$.diffHost, 'reload').returns(Promise.resolve());
sinon.stub(element, '_isFileUnchanged').returns(true);
sinon.spy(element, '_paramsChanged');
sinon.stub(element, '_getChangeDetail').returns(Promise.resolve({
...createChange(),
revisions: createRevisions(11),
}));
element.params = {
view: GerritNav.View.DIFF,
changeNum: '42',
path: '/COMMIT_MSG',
commentLink: true,
commentId: 'c1',
};
element._change = {
...createChange(),
revisions: createRevisions(11),
};
return element._paramsChanged.returnValues[0].then(() => {
assert.isTrue(diffNavStub.lastCall.calledWithExactly(
element._change, '/COMMIT_MSG', 2, 'PARENT', 10));
});
});
test('unchanged diff Base vs latest from comment does not navigate'
, () => {
const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
diffCommentsStub.returns(Promise.resolve({
'/COMMIT_MSG': [
{
...createComment(),
id: 'c1',
line: 10,
patch_set: 2,
path: '/COMMIT_MSG',
}, {
...createComment(),
id: 'c3',
line: 10,
patch_set: 'PARENT',
path: '/COMMIT_MSG',
},
]}));
sinon.stub(element.reporting, 'diffViewDisplayed');
sinon.stub(element, '_loadBlame');
sinon.stub(element.$.diffHost, 'reload').returns(Promise.resolve());
sinon.stub(element, '_isFileUnchanged').returns(true);
sinon.spy(element, '_paramsChanged');
sinon.stub(element, '_getChangeDetail').returns(Promise.resolve({
...createChange(),
revisions: createRevisions(11),
}));
element.params = {
view: GerritNav.View.DIFF,
changeNum: '42',
path: '/COMMIT_MSG',
commentLink: true,
commentId: 'c3',
};
element._change = {
...createChange(),
revisions: createRevisions(11),
};
return element._paramsChanged.returnValues[0].then(() => {
assert.isFalse(diffNavStub.called);
});
});
test('_isFileUnchanged', () => {
let diff = {
content: [
{a: 'abcd', ab: 'ef'},
{b: 'ancd', a: 'xx'},
],
};
assert.equal(element._isFileUnchanged(diff), false);
diff = {
content: [
{ab: 'abcd'},
{ab: 'ancd'},
],
};
assert.equal(element._isFileUnchanged(diff), true);
diff = {
content: [
{a: 'abcd', ab: 'ef', common: true},
{b: 'ancd', ab: 'xx'},
],
};
assert.equal(element._isFileUnchanged(diff), false);
diff = {
content: [
{a: 'abcd', ab: 'ef', common: true},
{b: 'ancd', ab: 'xx', common: true},
],
};
assert.equal(element._isFileUnchanged(diff), true);
});
test('diff toast to go to latest is shown and not base', async () => {
diffCommentsStub.returns(Promise.resolve({
'/COMMIT_MSG': [
{
...createComment(),
id: 'c1',
line: 10,
patch_set: 2,
path: '/COMMIT_MSG',
}, {
...createComment(),
id: 'c3',
line: 10,
patch_set: 'PARENT',
path: '/COMMIT_MSG',
},
]}));
sinon.stub(element.reporting, 'diffViewDisplayed');
sinon.stub(element, '_loadBlame');
sinon.stub(element.$.diffHost, 'reload').returns(Promise.resolve());
sinon.spy(element, '_paramsChanged');
getDiffChangeDetailStub.returns(
Promise.resolve({
...createChange(),
revisions: createRevisions(11),
}));
element._patchRange = {
patchNum: 2,
basePatchNum: 1,
};
sinon.stub(element, '_isFileUnchanged').returns(false);
const toastStub =
sinon.stub(element, '_displayDiffBaseAgainstLeftToast');
element.params = {
view: GerritNav.View.DIFF,
changeNum: '42',
project: 'p',
commentId: 'c1',
commentLink: true,
};
await element._paramsChanged.returnValues[0];
assert.isTrue(toastStub.called);
});
test('toggle left diff with a hotkey', () => {
const toggleLeftDiffStub = sinon.stub(
element.$.diffHost, 'toggleLeftDiff');
MockInteractions.pressAndReleaseKeyOn(element, 65, 'shift', 'a');
assert.isTrue(toggleLeftDiffStub.calledOnce);
});
test('keyboard shortcuts', () => {
element._changeNum = '42';
element._patchRange = {
basePatchNum: PARENT,
patchNum: 10,
};
element._change = {
_number: 42,
revisions: {
a: {_number: 10, commit: {parents: []}},
},
};
element._files = getFilesFromFileList(
['chell.go', 'glados.txt', 'wheatley.md']);
element._path = 'glados.txt';
element.changeViewState.selectedFileIndex = 1;
element._loggedIn = true;
const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
const changeNavStub = sinon.stub(GerritNav, 'navigateToChange');
MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u');
assert(changeNavStub.lastCall.calledWith(element._change),
'Should navigate to /c/42/');
MockInteractions.pressAndReleaseKeyOn(element, 221, null, ']');
assert(diffNavStub.lastCall.calledWith(element._change, 'wheatley.md',
10, PARENT), 'Should navigate to /c/42/10/wheatley.md');
element._path = 'wheatley.md';
assert.equal(element.changeViewState.selectedFileIndex, 2);
assert.isTrue(element._loading);
MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
assert(diffNavStub.lastCall.calledWith(element._change, 'glados.txt',
10, PARENT), 'Should navigate to /c/42/10/glados.txt');
element._path = 'glados.txt';
assert.equal(element.changeViewState.selectedFileIndex, 1);
assert.isTrue(element._loading);
MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
assert(diffNavStub.lastCall.calledWith(element._change, 'chell.go', 10,
PARENT), 'Should navigate to /c/42/10/chell.go');
element._path = 'chell.go';
assert.equal(element.changeViewState.selectedFileIndex, 0);
assert.isTrue(element._loading);
MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
assert(changeNavStub.lastCall.calledWith(element._change),
'Should navigate to /c/42/');
assert.equal(element.changeViewState.selectedFileIndex, 0);
assert.isTrue(element._loading);
const showPrefsStub =
sinon.stub(element.$.diffPreferencesDialog, 'open').callsFake(
() => Promise.resolve());
MockInteractions.pressAndReleaseKeyOn(element, 188, null, ',');
assert(showPrefsStub.calledOnce);
element.disableDiffPrefs = true;
MockInteractions.pressAndReleaseKeyOn(element, 188, null, ',');
assert(showPrefsStub.calledOnce);
let scrollStub = sinon.stub(element.cursor, 'moveToNextChunk');
MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'n');
assert(scrollStub.calledOnce);
scrollStub = sinon.stub(element.cursor, 'moveToPreviousChunk');
MockInteractions.pressAndReleaseKeyOn(element, 80, null, 'p');
assert(scrollStub.calledOnce);
scrollStub = sinon.stub(element.cursor, 'moveToNextCommentThread');
MockInteractions.pressAndReleaseKeyOn(element, 78, 'shift', 'n');
assert(scrollStub.calledOnce);
scrollStub = sinon.stub(element.cursor,
'moveToPreviousCommentThread');
MockInteractions.pressAndReleaseKeyOn(element, 80, 'shift', 'p');
assert(scrollStub.calledOnce);
const computeContainerClassStub = sinon.stub(element.$.diffHost.$.diff,
'_computeContainerClass');
MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
assert(computeContainerClassStub.lastCall.calledWithExactly(
false, 'SIDE_BY_SIDE', true));
MockInteractions.pressAndReleaseKeyOn(element, 27, null, 'esc');
assert(computeContainerClassStub.lastCall.calledWithExactly(
false, 'SIDE_BY_SIDE', false));
sinon.stub(element, '_setReviewed');
sinon.spy(element, '_handleToggleFileReviewed');
element.$.reviewed.checked = false;
MockInteractions.pressAndReleaseKeyOn(element, 82, 'shift', 'r');
assert.isFalse(element._setReviewed.called);
assert.isTrue(element._handleToggleFileReviewed.calledOnce);
MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
assert.isTrue(element._handleToggleFileReviewed.calledOnce);
clock.tick(1000);
MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
assert.isTrue(element._handleToggleFileReviewed.calledTwice);
assert.isTrue(element._setReviewed.called);
assert.equal(element._setReviewed.lastCall.args[0], true);
});
test('moveToNextCommentThread navigates to next file', () => {
const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
const diffChangeStub = sinon.stub(element, '_navigateToChange');
sinon.stub(element.cursor, 'isAtEnd').returns(true);
element._changeNum = '42';
const comment = {
'wheatley.md': [{
...createComment(),
patch_set: 10,
line: 21,
}],
};
element._changeComments = new ChangeComments(comment);
element._patchRange = {
basePatchNum: PARENT,
patchNum: 10,
};
element._change = {
_number: 42,
revisions: {
a: {_number: 10, commit: {parents: []}},
},
};
element._files = getFilesFromFileList(
['chell.go', 'glados.txt', 'wheatley.md']);
element._path = 'glados.txt';
element.changeViewState.selectedFileIndex = 1;
element._loggedIn = true;
MockInteractions.pressAndReleaseKeyOn(element, 78, 'shift', 'n');
flush();
assert.isTrue(diffNavStub.calledWithExactly(
element._change, 'wheatley.md', 10, PARENT, 21));
element._path = 'wheatley.md'; // navigated to next file
MockInteractions.pressAndReleaseKeyOn(element, 78, 'shift', 'n');
flush();
assert.isTrue(diffChangeStub.called);
});
test('shift+x shortcut toggles all diff context', () => {
const toggleStub = sinon.stub(element.$.diffHost, 'toggleAllContext');
MockInteractions.pressAndReleaseKeyOn(element, 88, 'shift', 'x');
flush();
assert.isTrue(toggleStub.called);
});
test('diff against base', () => {
element._patchRange = {
basePatchNum: 5,
patchNum: 10,
};
sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
element._handleDiffAgainstBase(new CustomEvent(''));
const args = diffNavStub.getCall(0).args;
assert.equal(args[2], 10);
assert.isNotOk(args[3]);
});
test('diff against latest', () => {
element._change = {
...createChange(),
revisions: createRevisions(12),
};
element._patchRange = {
basePatchNum: 5,
patchNum: 10,
};
sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
element._handleDiffAgainstLatest(new CustomEvent(''));
const args = diffNavStub.getCall(0).args;
assert.equal(args[2], 12);
assert.equal(args[3], 5);
});
test('_handleDiffBaseAgainstLeft', () => {
element._change = {
...createChange(),
revisions: createRevisions(10),
};
element._patchRange = {
patchNum: 3,
basePatchNum: 1,
};
element.params = {};
sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
element._handleDiffBaseAgainstLeft(new CustomEvent(''));
assert(diffNavStub.called);
const args = diffNavStub.getCall(0).args;
assert.equal(args[2], 1);
assert.equal(args[3], 'PARENT');
assert.isNotOk(args[4]);
});
test('_handleDiffBaseAgainstLeft when initially navigating to a comment',
() => {
element._change = {
...createChange(),
revisions: createRevisions(10),
};
element._patchRange = {
patchNum: 3,
basePatchNum: 1,
};
sinon.stub(element, '_paramsChanged');
element.params = {commentLink: true, view: GerritView.DIFF};
element._focusLineNum = 10;
sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
element._handleDiffBaseAgainstLeft(new CustomEvent(''));
assert(diffNavStub.called);
const args = diffNavStub.getCall(0).args;
assert.equal(args[2], 1);
assert.equal(args[3], 'PARENT');
assert.equal(args[4], 10);
});
test('_handleDiffRightAgainstLatest', () => {
element._change = {
...createChange(),
revisions: createRevisions(10),
};
element._patchRange = {
basePatchNum: 1,
patchNum: 3,
};
sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
element._handleDiffRightAgainstLatest(new CustomEvent(''));
assert(diffNavStub.called);
const args = diffNavStub.getCall(0).args;
assert.equal(args[2], 10);
assert.equal(args[3], 3);
});
test('_handleDiffBaseAgainstLatest', () => {
element._change = {
...createChange(),
revisions: createRevisions(10),
};
element._patchRange = {
basePatchNum: 1,
patchNum: 3,
};
sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
element._handleDiffBaseAgainstLatest(new CustomEvent(''));
assert(diffNavStub.called);
const args = diffNavStub.getCall(0).args;
assert.equal(args[2], 10);
assert.isNotOk(args[3]);
});
test('A fires an error event when not logged in', done => {
const changeNavStub = sinon.stub(GerritNav, 'navigateToChange');
sinon.stub(element, '_getLoggedIn').returns(Promise.resolve(false));
const loggedInErrorSpy = sinon.spy();
element.addEventListener('show-auth-required', loggedInErrorSpy);
MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
flush(() => {
assert.isTrue(changeNavStub.notCalled, 'The `a` keyboard shortcut ' +
'should only work when the user is logged in.');
assert.isNull(window.sessionStorage.getItem(
'changeView.showReplyDialog'));
assert.isTrue(loggedInErrorSpy.called);
done();
});
});
test('A navigates to change with logged in', done => {
element._changeNum = '42';
element._patchRange = {
basePatchNum: 5,
patchNum: 10,
};
element._change = {
_number: 42,
revisions: {
a: {_number: 10, commit: {parents: []}},
b: {_number: 5, commit: {parents: []}},
},
};
const changeNavStub = sinon.stub(GerritNav, 'navigateToChange');
sinon.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
const loggedInErrorSpy = sinon.spy();
element.addEventListener('show-auth-required', loggedInErrorSpy);
MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
flush(() => {
assert.isTrue(element.changeViewState.showReplyDialog);
assert(changeNavStub.lastCall.calledWithExactly(element._change, 10,
5), 'Should navigate to /c/42/5..10');
assert.isFalse(loggedInErrorSpy.called);
done();
});
});
test('A navigates to change with old patch number with logged in', done => {
element._changeNum = '42';
element._patchRange = {
basePatchNum: PARENT,
patchNum: 1,
};
element._change = {
_number: 42,
revisions: {
a: {_number: 1, commit: {parents: []}},
b: {_number: 2, commit: {parents: []}},
},
};
const changeNavStub = sinon.stub(GerritNav, 'navigateToChange');
sinon.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
const loggedInErrorSpy = sinon.spy();
element.addEventListener('show-auth-required', loggedInErrorSpy);
MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
flush(() => {
assert.isTrue(element.changeViewState.showReplyDialog);
assert(changeNavStub.lastCall.calledWithExactly(element._change, 1,
PARENT), 'Should navigate to /c/42/1');
assert.isFalse(loggedInErrorSpy.called);
done();
});
});
test('keyboard shortcuts with patch range', () => {
element._changeNum = '42';
element._patchRange = {
basePatchNum: 5,
patchNum: 10,
};
element._change = {
_number: 42,
revisions: {
a: {_number: 10, commit: {parents: []}},
b: {_number: 5, commit: {parents: []}},
},
};
element._files = getFilesFromFileList(
['chell.go', 'glados.txt', 'wheatley.md']);
element._path = 'glados.txt';
const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
const changeNavStub = sinon.stub(GerritNav, 'navigateToChange');
MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u');
assert(changeNavStub.lastCall.calledWithExactly(element._change, 10,
5), 'Should navigate to /c/42/5..10');
MockInteractions.pressAndReleaseKeyOn(element, 221, null, ']');
assert.isTrue(element._loading);
assert(diffNavStub.lastCall.calledWithExactly(element._change,
'wheatley.md', 10, 5, undefined),
'Should navigate to /c/42/5..10/wheatley.md');
element._path = 'wheatley.md';
MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
assert.isTrue(element._loading);
assert(diffNavStub.lastCall.calledWithExactly(element._change,
'glados.txt', 10, 5, undefined),
'Should navigate to /c/42/5..10/glados.txt');
element._path = 'glados.txt';
MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
assert.isTrue(element._loading);
assert(diffNavStub.lastCall.calledWithExactly(
element._change,
'chell.go',
10,
5,
undefined),
'Should navigate to /c/42/5..10/chell.go');
element._path = 'chell.go';
MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
assert.isTrue(element._loading);
assert(changeNavStub.lastCall.calledWithExactly(element._change, 10,
5),
'Should navigate to /c/42/5..10');
assert.isUndefined(element.changeViewState.showDownloadDialog);
MockInteractions.pressAndReleaseKeyOn(element, 68, null, 'd');
assert.isTrue(element.changeViewState.showDownloadDialog);
});
test('keyboard shortcuts with old patch number', () => {
element._changeNum = '42';
element._patchRange = {
basePatchNum: PARENT,
patchNum: 1,
};
element._change = {
_number: 42,
revisions: {
a: {_number: 1, commit: {parents: []}},
b: {_number: 2, commit: {parents: []}},
},
};
element._files = getFilesFromFileList(
['chell.go', 'glados.txt', 'wheatley.md']);
element._path = 'glados.txt';
const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
const changeNavStub = sinon.stub(GerritNav, 'navigateToChange');
MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u');
assert(changeNavStub.lastCall.calledWithExactly(element._change, 1,
PARENT), 'Should navigate to /c/42/1');
MockInteractions.pressAndReleaseKeyOn(element, 221, null, ']');
assert(diffNavStub.lastCall.calledWithExactly(element._change,
'wheatley.md', 1, PARENT, undefined),
'Should navigate to /c/42/1/wheatley.md');
element._path = 'wheatley.md';
MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
assert(diffNavStub.lastCall.calledWithExactly(element._change,
'glados.txt', 1, PARENT, undefined),
'Should navigate to /c/42/1/glados.txt');
element._path = 'glados.txt';
MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
assert(diffNavStub.lastCall.calledWithExactly(
element._change,
'chell.go',
1,
PARENT,
undefined), 'Should navigate to /c/42/1/chell.go');
element._path = 'chell.go';
changeNavStub.reset();
MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
assert(changeNavStub.lastCall.calledWithExactly(element._change, 1,
PARENT), 'Should navigate to /c/42/1');
assert.isTrue(changeNavStub.calledOnce);
});
test('edit should redirect to edit page', done => {
element._loggedIn = true;
element._path = 't.txt';
element._patchRange = {
basePatchNum: PARENT,
patchNum: 1,
};
element._change = {
_number: 42,
project: 'gerrit',
status: ChangeStatus.NEW,
revisions: {
a: {_number: 1, commit: {parents: []}},
b: {_number: 2, commit: {parents: []}},
},
};
const redirectStub = sinon.stub(GerritNav, 'navigateToRelativeUrl');
flush(() => {
const editBtn = element.shadowRoot
.querySelector('.editButton gr-button');
assert.isTrue(!!editBtn);
MockInteractions.tap(editBtn);
assert.isTrue(redirectStub.called);
assert.isTrue(redirectStub.lastCall.calledWithExactly(
GerritNav.getEditUrlForDiff(
element._change,
element._path,
element._patchRange.patchNum
)));
done();
});
});
test('edit should redirect to edit page with line number', done => {
const lineNumber = 42;
element._loggedIn = true;
element._path = 't.txt';
element._patchRange = {
basePatchNum: PARENT,
patchNum: 1,
};
element._change = {
_number: 42,
project: 'gerrit',
status: ChangeStatus.NEW,
revisions: {
a: {_number: 1, commit: {parents: []}},
b: {_number: 2, commit: {parents: []}},
},
};
sinon.stub(element.cursor, 'getAddress')
.returns({number: lineNumber, isLeftSide: false});
const redirectStub = sinon.stub(GerritNav, 'navigateToRelativeUrl');
flush(() => {
const editBtn = element.shadowRoot
.querySelector('.editButton gr-button');
assert.isTrue(!!editBtn);
MockInteractions.tap(editBtn);
assert.isTrue(redirectStub.called);
assert.isTrue(redirectStub.lastCall.calledWithExactly(
GerritNav.getEditUrlForDiff(
element._change,
element._path,
element._patchRange.patchNum,
lineNumber
)));
done();
});
});
function isEditVisibile({loggedIn, changeStatus}) {
return new Promise(resolve => {
element._loggedIn = loggedIn;
element._path = 't.txt';
element._patchRange = {
basePatchNum: PARENT,
patchNum: 1,
};
element._change = {
_number: 42,
status: changeStatus,
revisions: {
a: {_number: 1, commit: {parents: []}},
b: {_number: 2, commit: {parents: []}},
},
};
flush(() => {
const editBtn = element.shadowRoot
.querySelector('.editButton gr-button');
resolve(!!editBtn);
});
});
}
test('edit visible only when logged and status NEW', async () => {
for (const changeStatus of Object.keys(ChangeStatus)) {
assert.isFalse(await isEditVisibile({loggedIn: false, changeStatus}),
`loggedIn: false, changeStatus: ${changeStatus}`);
if (changeStatus !== ChangeStatus.NEW) {
assert.isFalse(await isEditVisibile({loggedIn: true, changeStatus}),
`loggedIn: true, changeStatus: ${changeStatus}`);
} else {
assert.isTrue(await isEditVisibile({loggedIn: true, changeStatus}),
`loggedIn: true, changeStatus: ${changeStatus}`);
}
}
});
test('edit visible when logged and status NEW', async () => {
assert.isTrue(await isEditVisibile(
{loggedIn: true, changeStatus: ChangeStatus.NEW}));
});
test('edit hidden when logged and status ABANDONED', async () => {
assert.isFalse(await isEditVisibile(
{loggedIn: true, changeStatus: ChangeStatus.ABANDONED}));
});
test('edit hidden when logged and status MERGED', async () => {
assert.isFalse(await isEditVisibile(
{loggedIn: true, changeStatus: ChangeStatus.MERGED}));
});
suite('diff prefs hidden', () => {
test('when no prefs or logged out', () => {
element.disableDiffPrefs = false;
element._loggedIn = false;
flush();
assert.isTrue(element.$.diffPrefsContainer.hidden);
element._loggedIn = true;
flush();
assert.isTrue(element.$.diffPrefsContainer.hidden);
element._loggedIn = false;
element._prefs = {font_size: '12'};
flush();
assert.isTrue(element.$.diffPrefsContainer.hidden);
element._loggedIn = true;
flush();
assert.isFalse(element.$.diffPrefsContainer.hidden);
});
test('when disableDiffPrefs is set', () => {
element._loggedIn = true;
element._prefs = {font_size: '12'};
element.disableDiffPrefs = false;
flush();
assert.isFalse(element.$.diffPrefsContainer.hidden);
element.disableDiffPrefs = true;
flush();
assert.isTrue(element.$.diffPrefsContainer.hidden);
});
});
test('prefsButton opens gr-diff-preferences', () => {
const handlePrefsTapSpy = sinon.spy(element, '_handlePrefsTap');
const overlayOpenStub = sinon.stub(element.$.diffPreferencesDialog,
'open');
const prefsButton =
element.root.querySelector('.prefsButton');
MockInteractions.tap(prefsButton);
assert.isTrue(handlePrefsTapSpy.called);
assert.isTrue(overlayOpenStub.called);
});
suite('url params', () => {
setup(() => {
sinon.stub(element, '_getFiles');
sinon.stub(
GerritNav,
'getUrlForDiff')
.callsFake((c, p, pn, bpn) => `${c._number}-${p}-${pn}-${bpn}`);
sinon.stub(
GerritNav
, 'getUrlForChange')
.callsFake((c, pn, bpn) => `${c._number}-${pn}-${bpn}`);
});
test('_formattedFiles', () => {
element._changeNum = '42';
element._patchRange = {
basePatchNum: PARENT,
patchNum: 10,
};
element._change = {_number: 42};
element._files = getFilesFromFileList(
['chell.go', 'glados.txt', 'wheatley.md',
'/COMMIT_MSG', '/MERGE_LIST']);
element._path = 'glados.txt';
const expectedFormattedFiles = [
{
text: 'chell.go',
mobileText: 'chell.go',
value: 'chell.go',
bottomText: '',
file: {
__path: 'chell.go',
},
}, {
text: 'glados.txt',
mobileText: 'glados.txt',
value: 'glados.txt',
bottomText: '',
file: {
__path: 'glados.txt',
},
}, {
text: 'wheatley.md',
mobileText: 'wheatley.md',
value: 'wheatley.md',
bottomText: '',
file: {
__path: 'wheatley.md',
},
},
{
text: 'Commit message',
mobileText: 'Commit message',
value: '/COMMIT_MSG',
bottomText: '',
file: {
__path: '/COMMIT_MSG',
},
},
{
text: 'Merge list',
mobileText: 'Merge list',
value: '/MERGE_LIST',
bottomText: '',
file: {
__path: '/MERGE_LIST',
},
},
];
assert.deepEqual(element._formattedFiles, expectedFormattedFiles);
assert.equal(element._formattedFiles[1].value, element._path);
});
test('prev/up/next links', () => {
element._changeNum = '42';
element._patchRange = {
basePatchNum: PARENT,
patchNum: 10,
};
element._change = {
_number: 42,
revisions: {
a: {_number: 10, commit: {parents: []}},
},
};
element._files = getFilesFromFileList(
['chell.go', 'glados.txt', 'wheatley.md']);
element._path = 'glados.txt';
flush();
const linkEls = element.root.querySelectorAll('.navLink');
assert.equal(linkEls.length, 3);
assert.equal(linkEls[0].getAttribute('href'), '42-chell.go-10-PARENT');
assert.equal(linkEls[1].getAttribute('href'), '42-undefined-undefined');
assert.equal(linkEls[2].getAttribute('href'),
'42-wheatley.md-10-PARENT');
element._path = 'wheatley.md';
flush();
assert.equal(linkEls[0].getAttribute('href'),
'42-glados.txt-10-PARENT');
assert.equal(linkEls[1].getAttribute('href'), '42-undefined-undefined');
assert.equal(linkEls[2].getAttribute('href'), '42-undefined-undefined');
element._path = 'chell.go';
flush();
assert.equal(linkEls[0].getAttribute('href'), '42-undefined-undefined');
assert.equal(linkEls[1].getAttribute('href'), '42-undefined-undefined');
assert.equal(linkEls[2].getAttribute('href'),
'42-glados.txt-10-PARENT');
element._path = 'not_a_real_file';
flush();
assert.equal(linkEls[0].getAttribute('href'),
'42-wheatley.md-10-PARENT');
assert.equal(linkEls[1].getAttribute('href'), '42-undefined-undefined');
assert.equal(linkEls[2].getAttribute('href'), '42-chell.go-10-PARENT');
});
test('prev/up/next links with patch range', () => {
element._changeNum = '42';
element._patchRange = {
basePatchNum: 5,
patchNum: 10,
};
element._change = {
_number: 42,
revisions: {
a: {_number: 5, commit: {parents: []}},
b: {_number: 10, commit: {parents: []}},
},
};
element._files = getFilesFromFileList(
['chell.go', 'glados.txt', 'wheatley.md']);
element._path = 'glados.txt';
flush();
const linkEls = element.root.querySelectorAll('.navLink');
assert.equal(linkEls.length, 3);
assert.equal(linkEls[0].getAttribute('href'), '42-chell.go-10-5');
assert.equal(linkEls[1].getAttribute('href'), '42-10-5');
assert.equal(linkEls[2].getAttribute('href'), '42-wheatley.md-10-5');
element._path = 'wheatley.md';
flush();
assert.equal(linkEls[0].getAttribute('href'), '42-glados.txt-10-5');
assert.equal(linkEls[1].getAttribute('href'), '42-10-5');
assert.equal(linkEls[2].getAttribute('href'), '42-10-5');
element._path = 'chell.go';
flush();
assert.equal(linkEls[0].getAttribute('href'),
'42-10-5');
assert.equal(linkEls[1].getAttribute('href'), '42-10-5');
assert.equal(linkEls[2].getAttribute('href'), '42-glados.txt-10-5');
});
});
test('_handlePatchChange calls navigateToDiff correctly', () => {
const navigateStub = sinon.stub(GerritNav, 'navigateToDiff');
element._change = {_number: 321, project: 'foo/bar'};
element._path = 'path/to/file.txt';
element._patchRange = {
basePatchNum: 'PARENT',
patchNum: 3,
};
const detail = {
basePatchNum: 'PARENT',
patchNum: 1,
};
element.$.rangeSelect.dispatchEvent(
new CustomEvent('patch-range-change', {detail, bubbles: false}));
assert(navigateStub.lastCall.calledWithExactly(element._change,
element._path, 1, 'PARENT'));
});
test('_prefs.manual_review is respected', () => {
const saveReviewedStub = sinon.stub(element, '_saveReviewedState')
.callsFake(() => Promise.resolve());
const getReviewedStub = sinon.stub(element, '_getReviewedStatus')
.returns(false);
sinon.stub(element.$.diffHost, 'reload');
element._loggedIn = true;
element._prefs = {manual_review: true};
element.params = {
view: GerritNav.View.DIFF,
changeNum: '42',
patchNum: 2,
basePatchNum: 1,
path: '/COMMIT_MSG',
};
element._patchRange = {
patchNum: 2,
basePatchNum: 1,
};
flush();
assert.isFalse(saveReviewedStub.called);
assert.isTrue(getReviewedStub.called);
const oldCount = getReviewedStub.callCount;
element._prefs = {};
element._path = 'abcd';
flush();
assert.isTrue(saveReviewedStub.called);
assert.equal(getReviewedStub.callCount, oldCount);
});
test('file review status', () => {
const saveReviewedStub = sinon.stub(element, '_saveReviewedState')
.callsFake(() => Promise.resolve());
sinon.stub(element.$.diffHost, 'reload');
element._loggedIn = true;
element.params = {
view: GerritNav.View.DIFF,
changeNum: '42',
patchNum: 2,
basePatchNum: 1,
path: '/COMMIT_MSG',
};
element._patchRange = {
patchNum: 2,
basePatchNum: 1,
};
element._path = 'abcd';
element._prefs = {};
flush();
const commitMsg = element.root.querySelector(
'input[type="checkbox"]');
assert.isTrue(commitMsg.checked);
MockInteractions.tap(commitMsg);
assert.isFalse(commitMsg.checked);
assert.isTrue(saveReviewedStub.lastCall.calledWithExactly(false));
MockInteractions.tap(commitMsg);
assert.isTrue(commitMsg.checked);
assert.isTrue(saveReviewedStub.lastCall.calledWithExactly(true));
const callCount = saveReviewedStub.callCount;
element.set('params.view', GerritNav.View.CHANGE);
flush();
// saveReviewedState observer observes params, but should not fire when
// view !== GerritNav.View.DIFF.
assert.equal(saveReviewedStub.callCount, callCount);
});
test('file review status with edit loaded', () => {
const saveReviewedStub = sinon.stub(element, '_saveReviewedState');
element._patchRange = {patchNum: EditPatchSetNum};
flush();
assert.isTrue(element._editMode);
element._setReviewed();
assert.isFalse(saveReviewedStub.called);
});
test('hash is determined from params', done => {
sinon.stub(element.$.diffHost, 'reload');
sinon.stub(element, '_initLineOfInterestAndCursor');
element._loggedIn = true;
element.params = {
view: GerritNav.View.DIFF,
changeNum: '42',
patchNum: 2,
basePatchNum: 1,
path: '/COMMIT_MSG',
hash: 10,
};
flush(() => {
assert.isTrue(element._initLineOfInterestAndCursor.calledOnce);
done();
});
});
test('diff mode selector correctly toggles the diff', () => {
const select = element.$.modeSelect;
const diffDisplay = element.$.diffHost;
element._userPrefs = {default_diff_view: 'SIDE_BY_SIDE'};
// The mode selected in the view state reflects the selected option.
assert.equal(element._getDiffViewMode(), select.mode);
// The mode selected in the view state reflects the view rednered in the
// diff.
assert.equal(select.mode, diffDisplay.viewMode);
// We will simulate a user change of the selected mode.
const newMode = 'UNIFIED_DIFF';
// Set the mode, and simulate the change event.
element.set('changeViewState.diffMode', newMode);
// Make sure the handler was called and the state is still coherent.
assert.equal(element._getDiffViewMode(), newMode);
assert.equal(element._getDiffViewMode(), select.mode);
assert.equal(element._getDiffViewMode(), diffDisplay.viewMode);
});
test('diff mode selector initializes from preferences', () => {
let resolvePrefs;
const prefsPromise = new Promise(resolve => {
resolvePrefs = resolve;
});
stubRestApi('getPreferences')
.callsFake(() => prefsPromise);
// Attach a new gr-diff-view so we can intercept the preferences fetch.
const view = document.createElement('gr-diff-view');
blankFixture.instantiate().appendChild(view);
flush();
// At this point the diff mode doesn't yet have the user's preference.
assert.equal(view._getDiffViewMode(), 'SIDE_BY_SIDE');
// Receive the overriding preference.
resolvePrefs({default_diff_view: 'UNIFIED'});
flush();
assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
});
test('diff mode selector should be hidden for binary', done => {
element._diff = {binary: true, content: []};
flush(() => {
const diffModeSelector = element.shadowRoot
.querySelector('.diffModeSelector');
assert.isTrue(diffModeSelector.classList.contains('hide'));
done();
});
});
suite('_commitRange', () => {
const change = {
_number: 42,
revisions: {
'commit-sha-1': {
_number: 1,
commit: {
parents: [{commit: 'sha-1-parent'}],
},
},
'commit-sha-2': {_number: 2, commit: {parents: []}},
'commit-sha-3': {_number: 3, commit: {parents: []}},
'commit-sha-4': {_number: 4, commit: {parents: []}},
'commit-sha-5': {
_number: 5,
commit: {
parents: [{commit: 'sha-5-parent'}],
},
},
},
};
setup(() => {
sinon.stub(element.$.diffHost, 'reload');
sinon.stub(element, '_initCursor');
element._change = change;
sinon.stub(element, '_getChangeDetail').returns(Promise.resolve(
change));
});
test('uses the patchNum and basePatchNum ', done => {
element.params = {
view: GerritNav.View.DIFF,
changeNum: '42',
patchNum: 4,
basePatchNum: 2,
path: '/COMMIT_MSG',
};
element._change = change;
flush(() => {
assert.deepEqual(element._commitRange, {
baseCommit: 'commit-sha-2',
commit: 'commit-sha-4',
});
done();
});
});
test('uses the parent when there is no base patch num ', done => {
element.params = {
view: GerritNav.View.DIFF,
changeNum: '42',
patchNum: 5,
path: '/COMMIT_MSG',
};
element._change = change;
flush(() => {
assert.deepEqual(element._commitRange, {
commit: 'commit-sha-5',
baseCommit: 'sha-5-parent',
});
done();
});
});
});
test('_initCursor', () => {
assert.isNotOk(element.cursor.initialLineNumber);
// Does nothing when params specify no cursor address:
element._initCursor(false);
assert.isNotOk(element.cursor.initialLineNumber);
// Does nothing when params specify side but no number:
element._initCursor(true);
assert.isNotOk(element.cursor.initialLineNumber);
// Revision hash: specifies lineNum but not side.
element._focusLineNum = 234;
element._initCursor(false);
assert.equal(element.cursor.initialLineNumber, 234);
assert.equal(element.cursor.side, 'right');
// Base hash: specifies lineNum and side.
element._focusLineNum = 345;
element._initCursor(true);
assert.equal(element.cursor.initialLineNumber, 345);
assert.equal(element.cursor.side, 'left');
// Specifies right side:
element._focusLineNum = 123;
element._initCursor(false);
assert.equal(element.cursor.initialLineNumber, 123);
assert.equal(element.cursor.side, 'right');
});
test('_getLineOfInterest', () => {
assert.isUndefined(element._getLineOfInterest(false));
element._focusLineNum = 12;
let result = element._getLineOfInterest(false);
assert.equal(result.number, 12);
assert.isNotOk(result.leftSide);
result = element._getLineOfInterest(true);
assert.equal(result.number, 12);
assert.isOk(result.leftSide);
});
test('_onLineSelected', () => {
const getUrlStub = sinon.stub(GerritNav, 'getUrlForDiffById');
const replaceStateStub = sinon.stub(history, 'replaceState');
sinon.stub(element.cursor, 'getAddress')
.returns({number: 123, isLeftSide: false});
element._changeNum = 321;
element._change = {_number: 321, project: 'foo/bar'};
element._patchRange = {
basePatchNum: 3,
patchNum: 5,
};
const e = {};
const detail = {number: 123, side: 'right'};
element._onLineSelected(e, detail);
assert.isTrue(replaceStateStub.called);
assert.isTrue(getUrlStub.called);
assert.isFalse(getUrlStub.lastCall.args[6]);
});
test('line selected on left side', () => {
const getUrlStub = sinon.stub(GerritNav, 'getUrlForDiffById');
const replaceStateStub = sinon.stub(history, 'replaceState');
sinon.stub(element.cursor, 'getAddress')
.returns({number: 123, isLeftSide: true});
element._changeNum = 321;
element._change = {_number: 321, project: 'foo/bar'};
element._patchRange = {
basePatchNum: 3,
patchNum: 5,
};
const e = {};
const detail = {number: 123, side: 'left'};
element._onLineSelected(e, detail);
assert.isTrue(replaceStateStub.called);
assert.isTrue(getUrlStub.called);
assert.isTrue(getUrlStub.lastCall.args[6]);
});
test('_getDiffViewMode', () => {
// No user prefs or change view state set.
assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
// User prefs but no change view state set.
element._userPrefs = {default_diff_view: 'UNIFIED_DIFF'};
assert.equal(element._getDiffViewMode(), 'UNIFIED_DIFF');
// User prefs and change view state set.
element.changeViewState = {diffMode: 'SIDE_BY_SIDE'};
assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
});
test('_handleToggleDiffMode', () => {
sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
const e = {preventDefault: () => {}};
// Initial state.
assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
element._handleToggleDiffMode(e);
assert.equal(element._getDiffViewMode(), 'UNIFIED_DIFF');
element._handleToggleDiffMode(e);
assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
});
suite('_initPatchRange', () => {
setup(async () => {
element.params = {
view: GerritView.DIFF,
changeNum: '42',
patchNum: 3,
};
await flush();
});
test('empty', () => {
sinon.stub(element, '_getPaths').returns(new Map());
element._initPatchRange();
assert.equal(Object.keys(element._commentMap).length, 0);
});
test('has paths', () => {
sinon.stub(element, '_getFiles');
sinon.stub(element, '_getPaths').returns({
'path/to/file/one.cpp': [{patch_set: 3, message: 'lorem'}],
'path-to/file/two.py': [{patch_set: 5, message: 'ipsum'}],
});
element._changeNum = '42';
element._patchRange = {
basePatchNum: 3,
patchNum: 5,
};
element._initPatchRange();
assert.deepEqual(Object.keys(element._commentMap),
['path/to/file/one.cpp', 'path-to/file/two.py']);
});
});
suite('_computeCommentSkips', () => {
test('empty file list', () => {
const commentMap = {
'path/one.jpg': true,
'path/three.wav': true,
};
const path = 'path/two.m4v';
const fileList = [];
const result = element._computeCommentSkips(commentMap, fileList, path);
assert.isNull(result.previous);
assert.isNull(result.next);
});
test('finds skips', () => {
const fileList = ['path/one.jpg', 'path/two.m4v', 'path/three.wav'];
let path = fileList[1];
const commentMap = {};
commentMap[fileList[0]] = true;
commentMap[fileList[1]] = false;
commentMap[fileList[2]] = true;
let result = element._computeCommentSkips(commentMap, fileList, path);
assert.equal(result.previous, fileList[0]);
assert.equal(result.next, fileList[2]);
commentMap[fileList[1]] = true;
result = element._computeCommentSkips(commentMap, fileList, path);
assert.equal(result.previous, fileList[0]);
assert.equal(result.next, fileList[2]);
path = fileList[0];
result = element._computeCommentSkips(commentMap, fileList, path);
assert.isNull(result.previous);
assert.equal(result.next, fileList[1]);
path = fileList[2];
result = element._computeCommentSkips(commentMap, fileList, path);
assert.equal(result.previous, fileList[1]);
assert.isNull(result.next);
});
suite('skip next/previous', () => {
let navToChangeStub;
let navToDiffStub;
setup(() => {
navToChangeStub = sinon.stub(element, '_navToChangeView');
navToDiffStub = sinon.stub(GerritNav, 'navigateToDiff');
element._files = getFilesFromFileList([
'path/one.jpg', 'path/two.m4v', 'path/three.wav',
]);
element._patchRange = {patchNum: 2, basePatchNum: 1};
});
suite('_moveToPreviousFileWithComment', () => {
test('no skips', () => {
element._moveToPreviousFileWithComment();
assert.isFalse(navToChangeStub.called);
assert.isFalse(navToDiffStub.called);
});
test('no previous', () => {
const commentMap = {};
commentMap[element._fileList[0]] = false;
commentMap[element._fileList[1]] = false;
commentMap[element._fileList[2]] = true;
element._commentMap = commentMap;
element._path = element._fileList[1];
element._moveToPreviousFileWithComment();
assert.isTrue(navToChangeStub.calledOnce);
assert.isFalse(navToDiffStub.called);
});
test('w/ previous', () => {
const commentMap = {};
commentMap[element._fileList[0]] = true;
commentMap[element._fileList[1]] = false;
commentMap[element._fileList[2]] = true;
element._commentMap = commentMap;
element._path = element._fileList[1];
element._moveToPreviousFileWithComment();
assert.isFalse(navToChangeStub.called);
assert.isTrue(navToDiffStub.calledOnce);
});
});
suite('_moveToNextFileWithComment', () => {
test('no skips', () => {
element._moveToNextFileWithComment();
assert.isFalse(navToChangeStub.called);
assert.isFalse(navToDiffStub.called);
});
test('no previous', () => {
const commentMap = {};
commentMap[element._fileList[0]] = true;
commentMap[element._fileList[1]] = false;
commentMap[element._fileList[2]] = false;
element._commentMap = commentMap;
element._path = element._fileList[1];
element._moveToNextFileWithComment();
assert.isTrue(navToChangeStub.calledOnce);
assert.isFalse(navToDiffStub.called);
});
test('w/ previous', () => {
const commentMap = {};
commentMap[element._fileList[0]] = true;
commentMap[element._fileList[1]] = false;
commentMap[element._fileList[2]] = true;
element._commentMap = commentMap;
element._path = element._fileList[1];
element._moveToNextFileWithComment();
assert.isFalse(navToChangeStub.called);
assert.isTrue(navToDiffStub.calledOnce);
});
});
});
});
test('_computeEditMode', () => {
const callCompute = range => element._computeEditMode({base: range});
assert.isFalse(callCompute({}));
assert.isFalse(callCompute({basePatchNum: 'PARENT', patchNum: 1}));
assert.isFalse(callCompute({basePatchNum: 'edit', patchNum: 1}));
assert.isTrue(callCompute({basePatchNum: 1, patchNum: 'edit'}));
});
test('_computeFileNum', () => {
assert.equal(element._computeFileNum('/foo',
[{value: '/foo'}, {value: '/bar'}]), 1);
assert.equal(element._computeFileNum('/bar',
[{value: '/foo'}, {value: '/bar'}]), 2);
});
test('_computeFileNumClass', () => {
assert.equal(element._computeFileNumClass(0, []), '');
assert.equal(element._computeFileNumClass(1,
[{value: '/foo'}, {value: '/bar'}]), 'show');
});
test('f open file dropdown', () => {
assert.isFalse(element.$.dropdown.$.dropdown.opened);
MockInteractions.pressAndReleaseKeyOn(element, 70, null, 'f');
flush();
assert.isTrue(element.$.dropdown.$.dropdown.opened);
});
suite('blame', () => {
test('toggle blame with button', () => {
const toggleBlame = sinon.stub(
element.$.diffHost, 'loadBlame')
.callsFake(() => Promise.resolve());
MockInteractions.tap(element.$.toggleBlame);
assert.isTrue(toggleBlame.calledOnce);
});
test('toggle blame with shortcut', () => {
const toggleBlame = sinon.stub(
element.$.diffHost, 'loadBlame').callsFake(() => Promise.resolve());
MockInteractions.pressAndReleaseKeyOn(element, 66, null, 'b');
assert.isTrue(toggleBlame.calledOnce);
});
});
suite('editMode behavior', () => {
setup(() => {
element._loggedIn = true;
});
const isVisible = el => {
assert.ok(el);
return getComputedStyle(el).getPropertyValue('display') !== 'none';
};
test('reviewed checkbox', () => {
sinon.stub(element, '_handlePatchChange');
element._patchRange = {patchNum: 1};
// Reviewed checkbox should be shown.
assert.isTrue(isVisible(element.$.reviewed));
element.set('_patchRange.patchNum', EditPatchSetNum);
flush();
assert.isFalse(isVisible(element.$.reviewed));
});
});
suite('switching files', () => {
let dispatchEventStub;
let navToFileStub;
let moveToPreviousChunkStub;
let moveToNextChunkStub;
let isAtStartStub;
let isAtEndStub;
let nowStub;
setup(() => {
dispatchEventStub = sinon.stub(
element, 'dispatchEvent').callThrough();
navToFileStub = sinon.stub(element, '_navToFile');
moveToPreviousChunkStub =
sinon.stub(element.cursor, 'moveToPreviousChunk');
moveToNextChunkStub =
sinon.stub(element.cursor, 'moveToNextChunk');
isAtStartStub = sinon.stub(element.cursor, 'isAtStart');
isAtEndStub = sinon.stub(element.cursor, 'isAtEnd');
nowStub = sinon.stub(Date, 'now');
});
test('shows toast when at the end of file', () => {
moveToNextChunkStub.returns(CursorMoveResult.CLIPPED);
isAtEndStub.returns(true);
MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'n');
assert.isTrue(moveToNextChunkStub.called);
assert.equal(dispatchEventStub.lastCall.args[0].type, 'show-alert');
assert.isFalse(navToFileStub.called);
});
test('navigates to next file when n is tapped again', () => {
moveToNextChunkStub.returns(CursorMoveResult.CLIPPED);
isAtEndStub.returns(true);
element._files = getFilesFromFileList(['file1', 'file2', 'file3']);
element._reviewedFiles = new Set(['file2']);
element._path = 'file1';
nowStub.returns(5);
MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'n');
nowStub.returns(10);
MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'n');
assert.isTrue(navToFileStub.called);
assert.deepEqual(navToFileStub.lastCall.args, [
'file1',
['file1', 'file3'],
1,
]);
});
test('does not navigate if n is tapped twice too slow', () => {
moveToNextChunkStub.returns(CursorMoveResult.CLIPPED);
isAtEndStub.returns(true);
nowStub.returns(5);
MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'n');
nowStub.returns(6000);
MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'n');
assert.isFalse(navToFileStub.called);
});
test('shows toast when at the start of file', () => {
moveToPreviousChunkStub.returns(CursorMoveResult.CLIPPED);
isAtStartStub.returns(true);
MockInteractions.pressAndReleaseKeyOn(element, 80, null, 'p');
assert.isTrue(moveToPreviousChunkStub.called);
assert.equal(dispatchEventStub.lastCall.args[0].type, 'show-alert');
assert.isFalse(navToFileStub.called);
});
test('navigates to prev file when p is tapped again', () => {
moveToPreviousChunkStub.returns(CursorMoveResult.CLIPPED);
isAtStartStub.returns(true);
element._files = getFilesFromFileList(['file1', 'file2', 'file3']);
element._reviewedFiles = new Set(['file2']);
element._path = 'file3';
nowStub.returns(5);
MockInteractions.pressAndReleaseKeyOn(element, 80, null, 'p');
nowStub.returns(10);
MockInteractions.pressAndReleaseKeyOn(element, 80, null, 'p');
assert.isTrue(navToFileStub.called);
assert.deepEqual(navToFileStub.lastCall.args, [
'file3',
['file1', 'file3'],
-1,
]);
});
test('does not navigate if p is tapped twice too slow', () => {
moveToPreviousChunkStub.returns(CursorMoveResult.CLIPPED);
isAtStartStub.returns(true);
nowStub.returns(5);
MockInteractions.pressAndReleaseKeyOn(element, 80, null, 'p');
nowStub.returns(6000);
MockInteractions.pressAndReleaseKeyOn(element, 80, null, 'p');
assert.isFalse(navToFileStub.called);
});
test('does not navigate when tapping n then p', () => {
moveToNextChunkStub.returns(CursorMoveResult.CLIPPED);
isAtEndStub.returns(true);
nowStub.returns(5);
MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'n');
moveToPreviousChunkStub.returns(CursorMoveResult.CLIPPED);
isAtStartStub.returns(true);
nowStub.returns(10);
MockInteractions.pressAndReleaseKeyOn(element, 80, null, 'p');
assert.isFalse(navToFileStub.called);
});
});
test('shift+m navigates to next unreviewed file', () => {
element._files = getFilesFromFileList(['file1', 'file2', 'file3']);
element._reviewedFiles = new Set(['file1', 'file2']);
element._path = 'file1';
const reviewedStub = sinon.stub(element, '_setReviewed');
const navStub = sinon.stub(element, '_navToFile');
MockInteractions.pressAndReleaseKeyOn(element, 77, 'shift', 'm');
flush();
assert.isTrue(reviewedStub.lastCall.args[0]);
assert.deepEqual(navStub.lastCall.args, [
'file1',
['file1', 'file3'],
1,
]);
});
test('File change should trigger navigateToDiff once', done => {
element._files = getFilesFromFileList(['file1', 'file2', 'file3']);
sinon.stub(element, '_initLineOfInterestAndCursor');
sinon.stub(GerritNav, 'navigateToDiff');
// Load file1
element.params = {
view: GerritNav.View.DIFF,
patchNum: 1,
changeNum: 101,
project: 'test-project',
path: 'file1',
};
element._patchRange = {
patchNum: 1,
basePatchNum: 'PARENT',
};
element._change = {
...createChange(),
revisions: createRevisions(1),
};
flush();
assert.isTrue(GerritNav.navigateToDiff.notCalled);
// Switch to file2
element._handleFileChange({detail: {value: 'file2'}});
assert.isTrue(GerritNav.navigateToDiff.calledOnce);
// This is to mock the param change triggered by above navigate
element.params = {
view: GerritNav.View.DIFF,
patchNum: 1,
changeNum: 101,
project: 'test-project',
path: 'file2',
};
element._patchRange = {
patchNum: 1,
basePatchNum: 'PARENT',
};
// No extra call
assert.isTrue(GerritNav.navigateToDiff.calledOnce);
done();
});
test('_computeDownloadDropdownLinks', () => {
const downloadLinks = [
{
url: '/changes/test~12/revisions/1/patch?zip&path=index.php',
name: 'Patch',
},
{
url: '/changes/test~12/revisions/1' +
'/files/index.php/download?parent=1',
name: 'Left Content',
},
{
url: '/changes/test~12/revisions/1' +
'/files/index.php/download',
name: 'Right Content',
},
];
const side = {
meta_a: true,
meta_b: true,
};
const base = {
patchNum: 1,
basePatchNum: 'PARENT',
};
assert.deepEqual(
element._computeDownloadDropdownLinks(
'test', 12, base, 'index.php', side),
downloadLinks);
});
test('_computeDownloadDropdownLinks diff returns renamed', () => {
const downloadLinks = [
{
url: '/changes/test~12/revisions/3/patch?zip&path=index.php',
name: 'Patch',
},
{
url: '/changes/test~12/revisions/2' +
'/files/index2.php/download',
name: 'Left Content',
},
{
url: '/changes/test~12/revisions/3' +
'/files/index.php/download',
name: 'Right Content',
},
];
const side = {
change_type: 'RENAMED',
meta_a: {
name: 'index2.php',
},
meta_b: true,
};
const base = {
patchNum: 3,
basePatchNum: 2,
};
assert.deepEqual(
element._computeDownloadDropdownLinks(
'test', 12, base, 'index.php', side),
downloadLinks);
});
test('_computeDownloadFileLink', () => {
const base = {
patchNum: 1,
basePatchNum: 'PARENT',
};
assert.equal(
element._computeDownloadFileLink(
'test', 12, base, 'index.php', true),
'/changes/test~12/revisions/1/files/index.php/download?parent=1');
assert.equal(
element._computeDownloadFileLink(
'test', 12, base, 'index.php', false),
'/changes/test~12/revisions/1/files/index.php/download');
});
test('_computeDownloadPatchLink', () => {
assert.equal(
element._computeDownloadPatchLink(
'test', 12, {patchNum: 1}, 'index.php'),
'/changes/test~12/revisions/1/patch?zip&path=index.php');
});
});
suite('unmodified files with comments', () => {
let element;
setup(() => {
const changedFiles = {
'file1.txt': {},
'a/b/test.c': {},
};
stubRestApi('getConfig').returns(Promise.resolve({change: {}}));
stubRestApi('getProjectConfig').returns(Promise.resolve({}));
stubRestApi('getDiffChangeDetail').returns(Promise.resolve({}));
stubRestApi('getChangeFiles').returns(Promise.resolve(changedFiles));
stubRestApi('saveFileReviewed').returns(Promise.resolve());
stubRestApi('getDiffComments').returns(Promise.resolve({}));
stubRestApi('getDiffRobotComments').returns(Promise.resolve({}));
stubRestApi('getDiffDrafts').returns(Promise.resolve({}));
stubRestApi('getReviewedFiles').returns(
Promise.resolve([]));
element = basicFixture.instantiate();
element._changeNum = '42';
return element._loadComments();
});
test('_getFiles add files with comments without changes', () => {
const patchChangeRecord = {
base: {
basePatchNum: 5,
patchNum: 10,
},
};
const changeComments = {
getPaths: sinon.stub().returns({
'file2.txt': {},
'file1.txt': {},
}),
};
return element._getFiles(23, patchChangeRecord, changeComments)
.then(() => {
assert.deepEqual(element._files, {
sortedFileList: ['a/b/test.c', 'file1.txt', 'file2.txt'],
changeFilesByPath: {
'file1.txt': {},
'file2.txt': {status: 'U'},
'a/b/test.c': {},
},
});
});
});
});
});