/** * @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, Side} from '../../../constants/constants.js'; import {TestKeyboardShortcutBinder} from '../../../test/test-utils.js'; import {SPECIAL_PATCH_SET_NUM} from '../../../utils/patch-set-util.js'; import {Shortcut} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js'; import {_testOnly_findCommentById} from '../gr-comment-api/gr-comment-api.js'; import {GerritView} from '../../core/gr-navigation/gr-navigation.js'; import { createChange, createRevisions, } from '../../../test/test-data-generators.js'; const basicFixture = fixtureFromElement('gr-diff-view'); const blankFixture = fixtureFromElement('div'); suite('gr-diff-view tests', () => { suite('basic tests', () => { let element; let clock; 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.EXPAND_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, }; } setup(async () => { clock = sinon.useFakeTimers(); stub('gr-rest-api-interface', { getConfig() { return Promise.resolve({change: {}}); }, getLoggedIn() { return Promise.resolve(false); }, getProjectConfig() { return Promise.resolve({}); }, getDiffChangeDetail() { return Promise.resolve({}); }, getChangeFiles() { return Promise.resolve({}); }, saveFileReviewed() { return Promise.resolve(); }, getDiffComments() { return Promise.resolve({}); }, getDiffRobotComments() { return Promise.resolve({}); }, getDiffDrafts() { return Promise.resolve({}); }, getPortedComments() { return Promise.resolve({}); }, getReviewedFiles() { return Promise.resolve([]); }, }); element = basicFixture.instantiate(); element._changeNum = '42'; element._path = 'some/path.txt'; element._change = {}; element._diff = {content: []}; element._patchRange = { patchNum: 77, basePatchNum: 'PARENT', }; sinon.stub(element.$.commentAPI, 'loadAll').returns(Promise.resolve({ _comments: {'/COMMIT_MSG': [ { id: 'c1', line: 10, patch_set: 2, diffSide: Side.LEFT, path: '/COMMIT_MSG', }, { id: 'c3', line: 10, patch_set: 'PARENT', diffSide: Side.LEFT, path: '/COMMIT_MSG', }, ]}, computeCommentThreadCount: () => {}, computeUnresolvedNum: () => {}, getPaths: () => {}, getThreadsBySideForFile: () => [], findCommentById: _testOnly_findCommentById, })); await element._loadComments(); 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); }); }); test('comment route', () => { const initLineOfInterestAndCursorStub = sinon.stub(element, '_initLineOfInterestAndCursor'); const getUrlStub = sinon.stub(GerritNav, 'getUrlForDiffById'); const 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), })); 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'); 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'); 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 () => { sinon.stub(element.reporting, 'diffViewDisplayed'); sinon.stub(element, '_loadBlame'); sinon.stub(element.$.diffHost, 'reload').returns(Promise.resolve()); sinon.spy(element, '_paramsChanged'); element.restApiService.getDiffChangeDetail.restore(); sinon.stub(element.restApiService, 'getDiffChangeDetail') .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('shift+x shortcut expands all diff context', () => { const expandStub = sinon.stub(element.$.diffHost, 'expandAllContext'); MockInteractions.pressAndReleaseKeyOn(element, 88, 'shift', 'x'); flush(); assert.isTrue(expandStub.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('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, 65, null, 'a'); assert.isTrue(changeNavStub.notCalled, 'The `a` keyboard shortcut ' + 'should only work when the user is logged in.'); assert.isNull(window.sessionStorage.getItem( 'changeView.showReplyDialog')); element._loggedIn = true; MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a'); assert.isTrue(element.changeViewState.showReplyDialog); assert(changeNavStub.lastCall.calledWithExactly(element._change, 10, 5), 'Should navigate to /c/42/5..10'); 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), '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), '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), '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, 65, null, 'a'); assert.isTrue(changeNavStub.notCalled, 'The `a` keyboard shortcut ' + 'should only work when the user is logged in.'); assert.isNull(window.sessionStorage.getItem( 'changeView.showReplyDialog')); element._loggedIn = true; MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a'); assert.isTrue(element.changeViewState.showReplyDialog); assert(changeNavStub.lastCall.calledWithExactly(element._change, 1, PARENT), 'Should navigate to /c/42/1'); 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), '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), '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), '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 in ChangeStatus) { if (!ChangeStatus.hasOwnProperty(changeStatus)) { continue; } 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); }); test('_computeCommentString', done => { const path = '/test'; element.$.commentAPI.loadAll().then(comments => { const commentThreadCountStub = sinon.stub(comments, 'computeCommentThreadCount'); const unresolvedCountStub = sinon.stub(comments, 'computeUnresolvedNum'); commentThreadCountStub.withArgs({patchNum: 1, path}).returns(0); commentThreadCountStub.withArgs({patchNum: 2, path}).returns(1); commentThreadCountStub.withArgs({patchNum: 3, path}).returns(2); commentThreadCountStub.withArgs({patchNum: 4, path}).returns(0); unresolvedCountStub.withArgs({patchNum: 1, path}).returns(1); unresolvedCountStub.withArgs({patchNum: 2, path}).returns(0); unresolvedCountStub.withArgs({patchNum: 3, path}).returns(2); unresolvedCountStub.withArgs({patchNum: 4, path}).returns(0); assert.equal(element._computeCommentString(comments, 1, path, {}), '1 unresolved'); assert.equal( element._computeCommentString(comments, 2, path, {status: 'M'}), '1 comment'); assert.equal( element._computeCommentString(comments, 2, path, {status: 'U'}), 'no changes, 1 comment'); assert.equal( element._computeCommentString(comments, 3, path, {status: 'A'}), '2 comments, 2 unresolved'); assert.equal( element._computeCommentString( comments, 4, path, {status: 'M'} ), ''); assert.equal( element._computeCommentString(comments, 4, path, {status: 'U'}), 'no changes'); done(); }); }); 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, }; // computeCommentThreadCount is an empty function hence stubbing // function that depends on it's return value sinon.stub(element, '_computeCommentString').returns(''); 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: '', }, { text: 'glados.txt', mobileText: 'glados.txt', value: 'glados.txt', bottomText: '', }, { text: 'wheatley.md', mobileText: 'wheatley.md', value: 'wheatley.md', bottomText: '', }, { text: 'Commit message', mobileText: 'Commit message', value: '/COMMIT_MSG', bottomText: '', }, { text: 'Merge list', mobileText: 'Merge list', value: '/MERGE_LIST', bottomText: '', }, ]; 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.isFalse(linkEls[2].hasAttribute('href')); element._path = 'chell.go'; flush(); assert.isFalse(linkEls[0].hasAttribute('href')); 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.isFalse(linkEls[2].hasAttribute('href')); element._path = 'chell.go'; flush(); assert.isFalse(linkEls[0].hasAttribute('href')); 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') .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._prefs = {manual_review: true}; flush(); assert.isFalse(saveReviewedStub.called); assert.isTrue(getReviewedStub.called); element._prefs = {}; flush(); assert.isTrue(saveReviewedStub.called); assert.isTrue(getReviewedStub.calledOnce); }); 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._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: SPECIAL_PATCH_SET_NUM.EDIT}; 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; }); sinon.stub(element.restApiService, '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('_getReviewedStatus', () => { const promises = []; element.restApiService.getReviewedFiles.restore(); sinon.stub(element.restApiService, 'getReviewedFiles') .returns(Promise.resolve(['path'])); promises.push(element._getReviewedStatus(true, null, null, 'path') .then(reviewed => assert.isFalse(reviewed))); promises.push(element._getReviewedStatus(false, null, null, 'otherPath') .then(reviewed => assert.isFalse(reviewed))); promises.push(element._getReviewedStatus(false, null, null, 'path') .then(reviewed => assert.isFalse(reviewed))); promises.push(element._getReviewedStatus(false, 3, 5, 'path') .then(reviewed => assert.isTrue(reviewed))); return Promise.all(promises); }); 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', SPECIAL_PATCH_SET_NUM.EDIT); flush(); assert.isFalse(isVisible(element.$.reviewed)); }); }); test('_paramsChanged sets in projectLookup', () => { sinon.stub(element, '_initLineOfInterestAndCursor'); const setStub = sinon.stub(element.restApiService, 'setInProjectLookup'); element._paramsChanged({ view: GerritNav.View.DIFF, changeNum: 101, project: 'test-project', path: '', }); assert.isTrue(setStub.calledOnce); assert.isTrue(setStub.calledWith(101, 'test-project')); }); 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('gr-diff-view tests unmodified files with comments', () => { let element; setup(() => { const changedFiles = { 'file1.txt': {}, 'a/b/test.c': {}, }; stub('gr-rest-api-interface', { getConfig() { return Promise.resolve({change: {}}); }, getLoggedIn() { return Promise.resolve(false); }, getProjectConfig() { return Promise.resolve({}); }, getDiffChangeDetail() { return Promise.resolve({}); }, getChangeFiles() { return Promise.resolve(changedFiles); }, saveFileReviewed() { return Promise.resolve(); }, getDiffComments() { return Promise.resolve({}); }, getDiffRobotComments() { return Promise.resolve({}); }, getDiffDrafts() { return Promise.resolve({}); }, getReviewedFiles() { return 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': {}, }, }); }); }); }); });