| /** |
| * @license |
| * Copyright 2015 Google LLC |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| import '../../../test/common-test-setup'; |
| import './gr-diff-view'; |
| import {navigationToken} from '../../core/gr-navigation/gr-navigation'; |
| import { |
| ChangeStatus, |
| DiffViewMode, |
| createDefaultDiffPrefs, |
| createDefaultPreferences, |
| } from '../../../constants/constants'; |
| import { |
| isVisible, |
| pressKey, |
| query, |
| queryAll, |
| queryAndAssert, |
| stubReporting, |
| stubRestApi, |
| stubUsers, |
| waitEventLoop, |
| waitUntil, |
| } from '../../../test/test-utils'; |
| import {ChangeComments} from '../gr-comment-api/gr-comment-api'; |
| import {GerritView} from '../../../services/router/router-model'; |
| import { |
| createRevisions, |
| createComment as createCommentGeneric, |
| TEST_NUMERIC_CHANGE_ID, |
| createDiff, |
| createPatchRange, |
| createServerInfo, |
| createConfig, |
| createParsedChange, |
| createRevision, |
| createCommit, |
| createFileInfo, |
| } from '../../../test/test-data-generators'; |
| import { |
| BasePatchSetNum, |
| CommentInfo, |
| CommitId, |
| EDIT, |
| FileInfo, |
| NumericChangeId, |
| PARENT, |
| PatchRange, |
| PatchSetNum, |
| PatchSetNumber, |
| PathToCommentsInfoMap, |
| RepoName, |
| RevisionPatchSetNum, |
| UrlEncodedCommentId, |
| } from '../../../types/common'; |
| import {CursorMoveResult} from '../../../api/core'; |
| import {DiffInfo, Side} from '../../../api/diff'; |
| import {Files, GrDiffView} from './gr-diff-view'; |
| import {DropdownItem} from '../../shared/gr-dropdown-list/gr-dropdown-list'; |
| import {SinonFakeTimers, SinonStub, SinonSpy} from 'sinon'; |
| import {LoadingStatus} from '../../../models/change/change-model'; |
| import {CommentMap} from '../../../utils/comment-util'; |
| import {ParsedChangeInfo} from '../../../types/types'; |
| import {assertIsDefined} from '../../../utils/common-util'; |
| import {GrDiffModeSelector} from '../../../embed/diff/gr-diff-mode-selector/gr-diff-mode-selector'; |
| import {fixture, html, assert} from '@open-wc/testing'; |
| import {EventType} from '../../../types/events'; |
| import {Key} from '../../../utils/dom-util'; |
| import {GrButton} from '../../shared/gr-button/gr-button'; |
| import {testResolver} from '../../../test/common-test-setup'; |
| |
| function createComment( |
| id: string, |
| line: number, |
| ps: number | PatchSetNum, |
| path: string |
| ): CommentInfo { |
| return { |
| ...createCommentGeneric(), |
| id: id as UrlEncodedCommentId, |
| line, |
| patch_set: ps as RevisionPatchSetNum, |
| path, |
| }; |
| } |
| |
| suite('gr-diff-view tests', () => { |
| suite('basic tests', () => { |
| let element: GrDiffView; |
| let clock: SinonFakeTimers; |
| let diffCommentsStub; |
| let getDiffRestApiStub: SinonStub; |
| let setUrlStub: SinonStub; |
| |
| function getFilesFromFileList(fileList: string[]): Files { |
| const changeFilesByPath = fileList.reduce((files, path) => { |
| files[path] = createFileInfo(); |
| return files; |
| }, {} as {[path: string]: FileInfo}); |
| return { |
| sortedFileList: fileList, |
| changeFilesByPath, |
| }; |
| } |
| |
| setup(async () => { |
| setUrlStub = sinon.stub(testResolver(navigationToken), 'setUrl'); |
| stubRestApi('getConfig').returns(Promise.resolve(createServerInfo())); |
| stubRestApi('getLoggedIn').returns(Promise.resolve(false)); |
| stubRestApi('getProjectConfig').returns(Promise.resolve(createConfig())); |
| stubRestApi('getChangeFiles').returns( |
| Promise.resolve({ |
| 'chell.go': createFileInfo(), |
| 'glados.txt': createFileInfo(), |
| 'wheatley.md': createFileInfo(), |
| }) |
| ); |
| stubRestApi('saveFileReviewed').returns(Promise.resolve(new Response())); |
| diffCommentsStub = stubRestApi('getDiffComments'); |
| diffCommentsStub.returns(Promise.resolve({})); |
| stubRestApi('getDiffRobotComments').returns(Promise.resolve({})); |
| stubRestApi('getDiffDrafts').returns(Promise.resolve({})); |
| stubRestApi('getPortedComments').returns(Promise.resolve({})); |
| |
| element = await fixture(html`<gr-diff-view></gr-diff-view>`); |
| element.changeNum = 42 as NumericChangeId; |
| element.path = 'some/path.txt'; |
| element.change = createParsedChange(); |
| element.diff = {...createDiff(), content: []}; |
| getDiffRestApiStub = stubRestApi('getDiff'); |
| // Delayed in case a test updates element.diff. |
| getDiffRestApiStub.callsFake(() => Promise.resolve(element.diff)); |
| element.patchRange = createPatchRange(); |
| element.changeComments = new ChangeComments({ |
| '/COMMIT_MSG': [ |
| createComment('c1', 10, 2, '/COMMIT_MSG'), |
| createComment('c3', 10, PARENT, '/COMMIT_MSG'), |
| ], |
| }); |
| await element.updateComplete; |
| |
| element.getCommentsModel().setState({ |
| comments: {}, |
| robotComments: {}, |
| drafts: {}, |
| portedComments: {}, |
| portedDrafts: {}, |
| discardedDrafts: [], |
| }); |
| }); |
| |
| teardown(() => { |
| clock && clock.restore(); |
| sinon.restore(); |
| }); |
| |
| test('viewState change triggers diffViewDisplayed()', () => { |
| const diffViewDisplayedStub = stubReporting('diffViewDisplayed'); |
| assertIsDefined(element.diffHost); |
| sinon.stub(element.diffHost, 'reload').returns(Promise.resolve()); |
| sinon.stub(element, 'initPatchRange'); |
| sinon.stub(element, 'fetchFiles'); |
| const viewStateChangedSpy = sinon.spy(element, 'viewStateChanged'); |
| element.viewState = { |
| view: GerritView.DIFF, |
| changeNum: 42 as NumericChangeId, |
| patchNum: 2 as RevisionPatchSetNum, |
| basePatchNum: 1 as BasePatchSetNum, |
| path: '/COMMIT_MSG', |
| }; |
| element.path = '/COMMIT_MSG'; |
| element.patchRange = createPatchRange(); |
| return viewStateChangedSpy.returnValues[0]?.then(() => { |
| assert.isTrue(diffViewDisplayedStub.calledOnce); |
| }); |
| }); |
| |
| suite('comment route', () => { |
| let initLineOfInterestAndCursorStub: SinonStub; |
| let replaceStateStub: SinonStub; |
| let viewStateChangedSpy: SinonSpy; |
| setup(() => { |
| initLineOfInterestAndCursorStub = sinon.stub( |
| element, |
| 'initLineOfInterestAndCursor' |
| ); |
| replaceStateStub = sinon.stub(history, 'replaceState'); |
| sinon.stub(element, 'fetchFiles'); |
| stubReporting('diffViewDisplayed'); |
| assertIsDefined(element.diffHost); |
| sinon.stub(element.diffHost, 'reload').returns(Promise.resolve()); |
| viewStateChangedSpy = sinon.spy(element, 'viewStateChanged'); |
| element.getChangeModel().setState({ |
| change: { |
| ...createParsedChange(), |
| revisions: createRevisions(11), |
| }, |
| loadingStatus: LoadingStatus.LOADED, |
| }); |
| }); |
| |
| test('comment url resolves to comment.patch_set vs latest', () => { |
| element.getCommentsModel().setState({ |
| comments: { |
| '/COMMIT_MSG': [ |
| createComment('c1', 10, 2, '/COMMIT_MSG'), |
| createComment('c3', 10, PARENT, '/COMMIT_MSG'), |
| ], |
| }, |
| robotComments: {}, |
| drafts: {}, |
| portedComments: {}, |
| portedDrafts: {}, |
| discardedDrafts: [], |
| }); |
| element.viewState = { |
| view: GerritView.DIFF, |
| changeNum: 42 as NumericChangeId, |
| commentLink: true, |
| commentId: 'c1' as UrlEncodedCommentId, |
| path: 'abcd', |
| patchNum: 1 as RevisionPatchSetNum, |
| }; |
| element.change = { |
| ...createParsedChange(), |
| revisions: createRevisions(11), |
| }; |
| return viewStateChangedSpy.returnValues[0].then(() => { |
| assert.isTrue( |
| initLineOfInterestAndCursorStub.calledWithExactly(true) |
| ); |
| assert.equal(element.focusLineNum, 10); |
| assert.equal(element.patchRange?.patchNum, 11 as RevisionPatchSetNum); |
| assert.equal(element.patchRange?.basePatchNum, 2 as BasePatchSetNum); |
| assert.isTrue(replaceStateStub.called); |
| }); |
| }); |
| }); |
| |
| test('viewState 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; |
| stubReporting('diffViewDisplayed'); |
| const loadBlameStub = sinon.stub(element, 'loadBlame'); |
| assertIsDefined(element.diffHost); |
| sinon.stub(element.diffHost, 'reload').returns(Promise.resolve()); |
| const viewStateChangedSpy = sinon.spy(element, 'viewStateChanged'); |
| sinon.stub(element, 'initPatchRange'); |
| sinon.stub(element, 'fetchFiles'); |
| element.viewState = { |
| view: GerritView.DIFF, |
| changeNum: 42 as NumericChangeId, |
| patchNum: 2 as RevisionPatchSetNum, |
| basePatchNum: 1 as BasePatchSetNum, |
| path: '/COMMIT_MSG', |
| }; |
| element.path = '/COMMIT_MSG'; |
| element.patchRange = createPatchRange(); |
| return viewStateChangedSpy.returnValues[0]!.then(() => { |
| assert.isTrue(element.isBlameLoaded); |
| assert.isTrue(loadBlameStub.calledOnce); |
| }); |
| }); |
| |
| test('unchanged diff X vs latest from comment links navigates to base vs X', async () => { |
| element.getCommentsModel().setState({ |
| comments: { |
| '/COMMIT_MSG': [ |
| createComment('c1', 10, 2, '/COMMIT_MSG'), |
| createComment('c3', 10, PARENT, '/COMMIT_MSG'), |
| ], |
| }, |
| robotComments: {}, |
| drafts: {}, |
| portedComments: {}, |
| portedDrafts: {}, |
| discardedDrafts: [], |
| }); |
| stubReporting('diffViewDisplayed'); |
| sinon.stub(element, 'loadBlame'); |
| assertIsDefined(element.diffHost); |
| sinon.stub(element.diffHost, 'reload').returns(Promise.resolve()); |
| sinon.stub(element, 'isFileUnchanged').returns(true); |
| const viewStateChangedSpy = sinon.spy(element, 'viewStateChanged'); |
| element.getChangeModel().setState({ |
| change: { |
| ...createParsedChange(), |
| revisions: createRevisions(11), |
| }, |
| loadingStatus: LoadingStatus.LOADED, |
| }); |
| element.viewState = { |
| view: GerritView.DIFF, |
| changeNum: 42 as NumericChangeId, |
| path: '/COMMIT_MSG', |
| commentLink: true, |
| commentId: 'c1' as UrlEncodedCommentId, |
| }; |
| element.change = { |
| ...createParsedChange(), |
| revisions: createRevisions(11), |
| }; |
| await viewStateChangedSpy.returnValues[0]; |
| assert.isTrue(setUrlStub.calledOnce); |
| assert.equal( |
| setUrlStub.lastCall.firstArg, |
| '/c/test-project/+/42/2//COMMIT_MSG#10' |
| ); |
| }); |
| |
| test('unchanged diff Base vs latest from comment does not navigate', async () => { |
| element.getCommentsModel().setState({ |
| comments: { |
| '/COMMIT_MSG': [ |
| createComment('c1', 10, 2, '/COMMIT_MSG'), |
| createComment('c3', 10, PARENT, '/COMMIT_MSG'), |
| ], |
| }, |
| robotComments: {}, |
| drafts: {}, |
| portedComments: {}, |
| portedDrafts: {}, |
| discardedDrafts: [], |
| }); |
| stubReporting('diffViewDisplayed'); |
| sinon.stub(element, 'loadBlame'); |
| assertIsDefined(element.diffHost); |
| sinon.stub(element.diffHost, 'reload').returns(Promise.resolve()); |
| sinon.stub(element, 'isFileUnchanged').returns(true); |
| const viewStateChangedSpy = sinon.spy(element, 'viewStateChanged'); |
| element.getChangeModel().setState({ |
| change: { |
| ...createParsedChange(), |
| revisions: createRevisions(11), |
| }, |
| loadingStatus: LoadingStatus.LOADED, |
| }); |
| element.viewState = { |
| view: GerritView.DIFF, |
| changeNum: 42 as NumericChangeId, |
| path: '/COMMIT_MSG', |
| commentLink: true, |
| commentId: 'c3' as UrlEncodedCommentId, |
| }; |
| element.change = { |
| ...createParsedChange(), |
| revisions: createRevisions(11), |
| }; |
| await viewStateChangedSpy.returnValues[0]; |
| assert.isFalse(setUrlStub.calledOnce); |
| }); |
| |
| test('isFileUnchanged', () => { |
| let diff: DiffInfo = { |
| ...createDiff(), |
| content: [ |
| {a: ['abcd'], ab: ['ef']}, |
| {b: ['ancd'], a: ['xx']}, |
| ], |
| }; |
| assert.equal(element.isFileUnchanged(diff), false); |
| diff = { |
| ...createDiff(), |
| content: [{ab: ['abcd']}, {ab: ['ancd']}], |
| }; |
| assert.equal(element.isFileUnchanged(diff), true); |
| diff = { |
| ...createDiff(), |
| content: [ |
| {a: ['abcd'], ab: ['ef'], common: true}, |
| {b: ['ancd'], ab: ['xx']}, |
| ], |
| }; |
| assert.equal(element.isFileUnchanged(diff), false); |
| diff = { |
| ...createDiff(), |
| 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 () => { |
| element.getCommentsModel().setState({ |
| comments: { |
| '/COMMIT_MSG': [ |
| createComment('c1', 10, 2, '/COMMIT_MSG'), |
| createComment('c3', 10, PARENT, '/COMMIT_MSG'), |
| ], |
| }, |
| robotComments: {}, |
| drafts: {}, |
| portedComments: {}, |
| portedDrafts: {}, |
| discardedDrafts: [], |
| }); |
| |
| stubReporting('diffViewDisplayed'); |
| sinon.stub(element, 'loadBlame'); |
| assertIsDefined(element.diffHost); |
| sinon.stub(element.diffHost, 'reload').returns(Promise.resolve()); |
| const viewStateChangedSpy = sinon.spy(element, 'viewStateChanged'); |
| element.change = undefined; |
| element.getChangeModel().setState({ |
| change: { |
| ...createParsedChange(), |
| revisions: createRevisions(11), |
| }, |
| loadingStatus: LoadingStatus.LOADED, |
| }); |
| element.patchRange = { |
| patchNum: 2 as RevisionPatchSetNum, |
| basePatchNum: 1 as BasePatchSetNum, |
| }; |
| sinon.stub(element, 'isFileUnchanged').returns(false); |
| const toastStub = sinon.stub(element, 'displayDiffBaseAgainstLeftToast'); |
| element.viewState = { |
| view: GerritView.DIFF, |
| changeNum: 42 as NumericChangeId, |
| project: 'p' as RepoName, |
| commentId: 'c1' as UrlEncodedCommentId, |
| commentLink: true, |
| }; |
| await viewStateChangedSpy.returnValues[0]; |
| assert.isTrue(toastStub.called); |
| }); |
| |
| test('toggle left diff with a hotkey', () => { |
| assertIsDefined(element.diffHost); |
| const toggleLeftDiffStub = sinon.stub(element.diffHost, 'toggleLeftDiff'); |
| pressKey(element, 'A'); |
| assert.isTrue(toggleLeftDiffStub.calledOnce); |
| }); |
| |
| test('renders', async () => { |
| clock = sinon.useFakeTimers(); |
| element.changeNum = 42 as NumericChangeId; |
| element.getBrowserModel().setScreenWidth(0); |
| element.patchRange = { |
| basePatchNum: PARENT, |
| patchNum: 10 as RevisionPatchSetNum, |
| }; |
| element.change = { |
| ...createParsedChange(), |
| _number: 42 as NumericChangeId, |
| revisions: { |
| a: createRevision(10), |
| }, |
| }; |
| element.files = getFilesFromFileList([ |
| 'chell.go', |
| 'glados.txt', |
| 'wheatley.md', |
| ]); |
| element.path = 'glados.txt'; |
| element.loggedIn = true; |
| await element.updateComplete; |
| assert.shadowDom.equal( |
| element, |
| /* HTML */ ` |
| <div class="stickyHeader"> |
| <h1 class="assistive-tech-only">Diff of glados.txt</h1> |
| <header> |
| <div> |
| <a href="/c/test-project/+/42"> 42 </a> |
| <span class="changeNumberColon"> : </span> |
| <span class="headerSubject"> Test subject </span> |
| <input |
| aria-label="file reviewed" |
| class="hideOnEdit reviewed" |
| id="reviewed" |
| title="Toggle reviewed status of file" |
| type="checkbox" |
| /> |
| <div class="jumpToFileContainer"> |
| <gr-dropdown-list id="dropdown" show-copy-for-trigger-text=""> |
| </gr-dropdown-list> |
| </div> |
| </div> |
| <div class="desktop navLinks"> |
| <span class="fileNum show"> |
| File 2 of 3 |
| <span class="separator"> </span> |
| </span> |
| <a |
| class="navLink" |
| href="/c/test-project/+/42/10/chell.go" |
| title="Go to previous file (shortcut: [)" |
| > |
| Prev |
| </a> |
| <span class="separator"> </span> |
| <a |
| class="navLink" |
| href="/c/test-project/+/42" |
| title="Up to change (shortcut: u)" |
| > |
| Up |
| </a> |
| <span class="separator"> </span> |
| <a |
| class="navLink" |
| href="/c/test-project/+/42/10/wheatley.md" |
| title="Go to next file (shortcut: ])" |
| > |
| Next |
| </a> |
| </div> |
| </header> |
| <div class="subHeader"> |
| <div class="patchRangeLeft"> |
| <gr-patch-range-select id="rangeSelect"> |
| </gr-patch-range-select> |
| <span class="desktop download"> |
| <span class="separator"> </span> |
| <gr-dropdown down-arrow="" horizontal-align="left" link=""> |
| <span class="downloadTitle"> Download </span> |
| </gr-dropdown> |
| </span> |
| </div> |
| <div class="rightControls"> |
| <span class="blameLoader show"> |
| <gr-button |
| aria-disabled="false" |
| id="toggleBlame" |
| link="" |
| role="button" |
| tabindex="0" |
| title="Toggle blame (shortcut: b)" |
| > |
| Show blame |
| </gr-button> |
| </span> |
| <span class="separator"> </span> |
| <span class="editButton"> |
| <gr-button |
| aria-disabled="false" |
| link="" |
| role="button" |
| tabindex="0" |
| title="Edit current file" |
| > |
| edit |
| </gr-button> |
| </span> |
| <span class="separator"> </span> |
| <div class="diffModeSelector"> |
| <span> Diff view: </span> |
| <gr-diff-mode-selector id="modeSelect" show-tooltip-below=""> |
| </gr-diff-mode-selector> |
| </div> |
| <span id="diffPrefsContainer"> |
| <span class="desktop preferences"> |
| <gr-tooltip-content |
| has-tooltip="" |
| position-below="" |
| title="Diff preferences" |
| > |
| <gr-button |
| aria-disabled="false" |
| class="prefsButton" |
| link="" |
| role="button" |
| tabindex="0" |
| > |
| <gr-icon icon="settings" filled></gr-icon> |
| </gr-button> |
| </gr-tooltip-content> |
| </span> |
| </span> |
| <gr-endpoint-decorator name="annotation-toggler"> |
| <span hidden="" id="annotation-span"> |
| <label for="annotation-checkbox" id="annotation-label"> |
| </label> |
| <iron-input> |
| <input |
| disabled="" |
| id="annotation-checkbox" |
| is="iron-input" |
| type="checkbox" |
| value="" |
| /> |
| </iron-input> |
| </span> |
| </gr-endpoint-decorator> |
| </div> |
| </div> |
| <div class="fileNav mobile"> |
| <a class="mobileNavLink" href="/c/test-project/+/42/10/chell.go"> |
| < |
| </a> |
| <div class="fullFileName mobile">glados.txt</div> |
| <a |
| class="mobileNavLink" |
| href="/c/test-project/+/42/10/wheatley.md" |
| > |
| > |
| </a> |
| </div> |
| </div> |
| <div class="loading">Loading...</div> |
| <h2 class="assistive-tech-only">Diff view</h2> |
| <gr-diff-host hidden="" id="diffHost"> </gr-diff-host> |
| <gr-apply-fix-dialog id="applyFixDialog"> </gr-apply-fix-dialog> |
| <gr-diff-preferences-dialog id="diffPreferencesDialog"> |
| </gr-diff-preferences-dialog> |
| <gr-overlay |
| aria-hidden="true" |
| id="downloadOverlay" |
| style="outline: none; display: none;" |
| > |
| <gr-download-dialog id="downloadDialog" role="dialog"> |
| </gr-download-dialog> |
| </gr-overlay> |
| ` |
| ); |
| }); |
| |
| test('keyboard shortcuts', async () => { |
| clock = sinon.useFakeTimers(); |
| element.changeNum = 42 as NumericChangeId; |
| element.getBrowserModel().setScreenWidth(0); |
| element.patchRange = { |
| basePatchNum: PARENT, |
| patchNum: 10 as RevisionPatchSetNum, |
| }; |
| element.change = { |
| ...createParsedChange(), |
| _number: 42 as NumericChangeId, |
| revisions: { |
| a: createRevision(10), |
| }, |
| }; |
| element.files = getFilesFromFileList([ |
| 'chell.go', |
| 'glados.txt', |
| 'wheatley.md', |
| ]); |
| element.path = 'glados.txt'; |
| element.loggedIn = true; |
| await element.updateComplete; |
| setUrlStub.reset(); |
| |
| pressKey(element, 'u'); |
| assert.equal(setUrlStub.callCount, 1); |
| assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42'); |
| await element.updateComplete; |
| |
| pressKey(element, ']'); |
| assert.equal(setUrlStub.callCount, 2); |
| assert.equal( |
| setUrlStub.lastCall.firstArg, |
| '/c/test-project/+/42/10/wheatley.md' |
| ); |
| element.path = 'wheatley.md'; |
| await element.updateComplete; |
| |
| assert.isTrue(element.loading); |
| |
| pressKey(element, '['); |
| assert.equal(setUrlStub.callCount, 3); |
| assert.equal( |
| setUrlStub.lastCall.firstArg, |
| '/c/test-project/+/42/10/glados.txt' |
| ); |
| element.path = 'glados.txt'; |
| await element.updateComplete; |
| |
| assert.isTrue(element.loading); |
| |
| pressKey(element, '['); |
| assert.equal(setUrlStub.callCount, 4); |
| assert.equal( |
| setUrlStub.lastCall.firstArg, |
| '/c/test-project/+/42/10/chell.go' |
| ); |
| element.path = 'chell.go'; |
| await element.updateComplete; |
| |
| assert.isTrue(element.loading); |
| |
| pressKey(element, '['); |
| assert.equal(setUrlStub.callCount, 5); |
| assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42'); |
| await element.updateComplete; |
| assert.isTrue(element.loading); |
| |
| assertIsDefined(element.diffPreferencesDialog); |
| const showPrefsStub = sinon |
| .stub(element.diffPreferencesDialog, 'open') |
| .callsFake(() => Promise.resolve()); |
| |
| pressKey(element, ','); |
| await element.updateComplete; |
| assert(showPrefsStub.calledOnce); |
| |
| assertIsDefined(element.cursor); |
| let scrollStub = sinon.stub(element.cursor, 'moveToNextChunk'); |
| pressKey(element, 'n'); |
| await element.updateComplete; |
| assert(scrollStub.calledOnce); |
| |
| scrollStub = sinon.stub(element.cursor, 'moveToPreviousChunk'); |
| pressKey(element, 'p'); |
| await element.updateComplete; |
| assert(scrollStub.calledOnce); |
| |
| scrollStub = sinon.stub(element.cursor, 'moveToNextCommentThread'); |
| pressKey(element, 'N'); |
| await element.updateComplete; |
| assert(scrollStub.calledOnce); |
| |
| scrollStub = sinon.stub(element.cursor, 'moveToPreviousCommentThread'); |
| pressKey(element, 'P'); |
| await element.updateComplete; |
| assert(scrollStub.calledOnce); |
| |
| assertIsDefined(element.diffHost); |
| assertIsDefined(element.diffHost.diffElement); |
| pressKey(element, 'j'); |
| await element.updateComplete; |
| assert.equal( |
| element.diffHost.diffElement.viewMode, |
| DiffViewMode.SIDE_BY_SIDE |
| ); |
| assert.isTrue(element.diffHost.diffElement.displayLine); |
| |
| pressKey(element, Key.ESC); |
| await element.updateComplete; |
| assert.equal( |
| element.diffHost.diffElement.viewMode, |
| DiffViewMode.SIDE_BY_SIDE |
| ); |
| assert.isFalse(element.diffHost.diffElement.displayLine); |
| |
| // Note that stubbing setReviewed means that the value of the |
| // `element.reviewed` checkbox is not flipped. |
| const setReviewedStub = sinon.stub(element, 'setReviewed'); |
| const handleToggleSpy = sinon.spy(element, 'handleToggleFileReviewed'); |
| assertIsDefined(element.reviewed); |
| element.reviewed.checked = false; |
| assert.isFalse(handleToggleSpy.called); |
| assert.isFalse(setReviewedStub.called); |
| |
| pressKey(element, 'r'); |
| assert.isTrue(handleToggleSpy.calledOnce); |
| assert.isTrue(setReviewedStub.calledOnce); |
| assert.equal(setReviewedStub.lastCall.args[0], true); |
| |
| // Handler is throttled, so another key press within 500 ms is ignored. |
| clock.tick(100); |
| pressKey(element, 'r'); |
| assert.isTrue(handleToggleSpy.calledOnce); |
| assert.isTrue(setReviewedStub.calledOnce); |
| |
| clock.tick(1000); |
| pressKey(element, 'r'); |
| assert.isTrue(handleToggleSpy.calledTwice); |
| assert.isTrue(setReviewedStub.calledTwice); |
| clock.restore(); |
| }); |
| |
| test('moveToNextCommentThread navigates to next file', async () => { |
| assertIsDefined(element.cursor); |
| sinon.stub(element.cursor, 'isAtEnd').returns(true); |
| element.changeNum = 42 as NumericChangeId; |
| const comment: PathToCommentsInfoMap = { |
| 'wheatley.md': [createComment('c2', 21, 10, 'wheatley.md')], |
| }; |
| element.changeComments = new ChangeComments(comment); |
| element.patchRange = { |
| basePatchNum: PARENT, |
| patchNum: 10 as RevisionPatchSetNum, |
| }; |
| element.change = { |
| ...createParsedChange(), |
| _number: 42 as NumericChangeId, |
| revisions: { |
| a: createRevision(10), |
| }, |
| }; |
| element.files = getFilesFromFileList([ |
| 'chell.go', |
| 'glados.txt', |
| 'wheatley.md', |
| ]); |
| element.path = 'glados.txt'; |
| element.loggedIn = true; |
| await element.updateComplete; |
| setUrlStub.reset(); |
| |
| pressKey(element, 'N'); |
| await element.updateComplete; |
| assert.equal(setUrlStub.callCount, 1); |
| assert.equal( |
| setUrlStub.lastCall.firstArg, |
| '/c/test-project/+/42/10/wheatley.md#21' |
| ); |
| |
| element.path = 'wheatley.md'; // navigated to next file |
| |
| pressKey(element, 'N'); |
| await element.updateComplete; |
| |
| assert.equal(setUrlStub.callCount, 2); |
| assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42'); |
| }); |
| |
| test('shift+x shortcut toggles all diff context', async () => { |
| assertIsDefined(element.diffHost); |
| const toggleStub = sinon.stub(element.diffHost, 'toggleAllContext'); |
| pressKey(element, 'X'); |
| await element.updateComplete; |
| assert.isTrue(toggleStub.called); |
| }); |
| |
| test('diff against base', async () => { |
| element.patchRange = { |
| basePatchNum: 5 as BasePatchSetNum, |
| patchNum: 10 as RevisionPatchSetNum, |
| }; |
| await element.updateComplete; |
| element.handleDiffAgainstBase(); |
| assert.equal( |
| setUrlStub.lastCall.firstArg, |
| '/c/test-project/+/42/10/some/path.txt' |
| ); |
| }); |
| |
| test('diff against latest', async () => { |
| element.path = 'foo'; |
| element.change = { |
| ...createParsedChange(), |
| revisions: createRevisions(12), |
| }; |
| element.patchRange = { |
| basePatchNum: 5 as BasePatchSetNum, |
| patchNum: 10 as RevisionPatchSetNum, |
| }; |
| await element.updateComplete; |
| element.handleDiffAgainstLatest(); |
| assert.equal( |
| setUrlStub.lastCall.firstArg, |
| '/c/test-project/+/42/5..12/foo' |
| ); |
| }); |
| |
| test('handleDiffBaseAgainstLeft', async () => { |
| element.path = 'foo'; |
| element.change = { |
| ...createParsedChange(), |
| revisions: createRevisions(10), |
| }; |
| element.patchRange = { |
| patchNum: 3 as RevisionPatchSetNum, |
| basePatchNum: 1 as BasePatchSetNum, |
| }; |
| element.viewState = { |
| view: GerritView.DIFF, |
| changeNum: 42 as NumericChangeId, |
| patchNum: 3 as RevisionPatchSetNum, |
| basePatchNum: 1 as BasePatchSetNum, |
| path: 'foo', |
| }; |
| await element.updateComplete; |
| element.handleDiffBaseAgainstLeft(); |
| assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42/1/foo'); |
| }); |
| |
| test('handleDiffBaseAgainstLeft when initially navigating to a comment', () => { |
| element.change = { |
| ...createParsedChange(), |
| revisions: createRevisions(10), |
| }; |
| element.patchRange = { |
| patchNum: 3 as RevisionPatchSetNum, |
| basePatchNum: 1 as BasePatchSetNum, |
| }; |
| sinon.stub(element, 'viewStateChanged'); |
| element.viewState = { |
| commentLink: true, |
| view: GerritView.DIFF, |
| changeNum: 42 as NumericChangeId, |
| }; |
| element.focusLineNum = 10; |
| element.handleDiffBaseAgainstLeft(); |
| assert.equal( |
| setUrlStub.lastCall.firstArg, |
| '/c/test-project/+/42/1/some/path.txt#10' |
| ); |
| }); |
| |
| test('handleDiffRightAgainstLatest', async () => { |
| element.path = 'foo'; |
| element.change = { |
| ...createParsedChange(), |
| revisions: createRevisions(10), |
| }; |
| element.patchRange = { |
| basePatchNum: 1 as BasePatchSetNum, |
| patchNum: 3 as RevisionPatchSetNum, |
| }; |
| await element.updateComplete; |
| element.handleDiffRightAgainstLatest(); |
| assert.equal( |
| setUrlStub.lastCall.firstArg, |
| '/c/test-project/+/42/3..10/foo' |
| ); |
| }); |
| |
| test('handleDiffBaseAgainstLatest', async () => { |
| element.change = { |
| ...createParsedChange(), |
| revisions: createRevisions(10), |
| }; |
| element.patchRange = { |
| basePatchNum: 1 as BasePatchSetNum, |
| patchNum: 3 as RevisionPatchSetNum, |
| }; |
| await element.updateComplete; |
| element.handleDiffBaseAgainstLatest(); |
| assert.equal( |
| setUrlStub.lastCall.firstArg, |
| '/c/test-project/+/42/10/some/path.txt' |
| ); |
| }); |
| |
| test('A fires an error event when not logged in', async () => { |
| element.loggedIn = false; |
| const loggedInErrorSpy = sinon.spy(); |
| element.addEventListener('show-auth-required', loggedInErrorSpy); |
| pressKey(element, 'a'); |
| await element.updateComplete; |
| assert.isFalse(setUrlStub.calledOnce); |
| assert.isTrue(loggedInErrorSpy.called); |
| }); |
| |
| test('A navigates to change with logged in', async () => { |
| element.changeNum = 42 as NumericChangeId; |
| element.patchRange = { |
| basePatchNum: 5 as BasePatchSetNum, |
| patchNum: 10 as RevisionPatchSetNum, |
| }; |
| element.change = { |
| ...createParsedChange(), |
| _number: 42 as NumericChangeId, |
| revisions: { |
| a: createRevision(10), |
| b: createRevision(5), |
| }, |
| }; |
| element.loggedIn = true; |
| await element.updateComplete; |
| const loggedInErrorSpy = sinon.spy(); |
| element.addEventListener('show-auth-required', loggedInErrorSpy); |
| setUrlStub.reset(); |
| |
| pressKey(element, 'a'); |
| |
| await element.updateComplete; |
| assert.equal(setUrlStub.callCount, 1); |
| assert.equal( |
| setUrlStub.lastCall.firstArg, |
| '/c/test-project/+/42/5..10?openReplyDialog=true' |
| ); |
| assert.isFalse(loggedInErrorSpy.called); |
| }); |
| |
| test('A navigates to change with old patch number with logged in', async () => { |
| element.changeNum = 42 as NumericChangeId; |
| element.patchRange = { |
| basePatchNum: PARENT, |
| patchNum: 1 as RevisionPatchSetNum, |
| }; |
| element.change = { |
| ...createParsedChange(), |
| _number: 42 as NumericChangeId, |
| revisions: { |
| a: createRevision(1), |
| b: createRevision(2), |
| }, |
| }; |
| element.loggedIn = true; |
| const loggedInErrorSpy = sinon.spy(); |
| element.addEventListener('show-auth-required', loggedInErrorSpy); |
| pressKey(element, 'a'); |
| await element.updateComplete; |
| assert.isTrue(setUrlStub.calledOnce); |
| assert.equal( |
| setUrlStub.lastCall.firstArg, |
| '/c/test-project/+/42/1?openReplyDialog=true' |
| ); |
| assert.isFalse(loggedInErrorSpy.called); |
| }); |
| |
| test('keyboard shortcuts with patch range', () => { |
| element.changeNum = 42 as NumericChangeId; |
| element.patchRange = { |
| basePatchNum: 5 as BasePatchSetNum, |
| patchNum: 10 as RevisionPatchSetNum, |
| }; |
| element.change = { |
| ...createParsedChange(), |
| _number: 42 as NumericChangeId, |
| revisions: { |
| a: createRevision(10), |
| b: createRevision(5), |
| }, |
| }; |
| element.files = getFilesFromFileList([ |
| 'chell.go', |
| 'glados.txt', |
| 'wheatley.md', |
| ]); |
| element.path = 'glados.txt'; |
| |
| pressKey(element, 'u'); |
| assert.equal(setUrlStub.callCount, 1); |
| assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42/5..10'); |
| |
| pressKey(element, ']'); |
| assert.isTrue(element.loading); |
| assert.equal(setUrlStub.callCount, 2); |
| assert.equal( |
| setUrlStub.lastCall.firstArg, |
| '/c/test-project/+/42/5..10/wheatley.md' |
| ); |
| element.path = 'wheatley.md'; |
| |
| pressKey(element, '['); |
| assert.isTrue(element.loading); |
| assert.equal(setUrlStub.callCount, 3); |
| assert.equal( |
| setUrlStub.lastCall.firstArg, |
| '/c/test-project/+/42/5..10/glados.txt' |
| ); |
| element.path = 'glados.txt'; |
| |
| pressKey(element, '['); |
| assert.isTrue(element.loading); |
| assert.equal(setUrlStub.callCount, 4); |
| assert.equal( |
| setUrlStub.lastCall.firstArg, |
| '/c/test-project/+/42/5..10/chell.go' |
| ); |
| element.path = 'chell.go'; |
| |
| pressKey(element, '['); |
| assert.isTrue(element.loading); |
| assert.equal(setUrlStub.callCount, 5); |
| assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42/5..10'); |
| |
| assertIsDefined(element.downloadOverlay); |
| const downloadOverlayStub = sinon |
| .stub(element.downloadOverlay, 'open') |
| .returns(Promise.resolve()); |
| pressKey(element, 'd'); |
| assert.isTrue(downloadOverlayStub.called); |
| }); |
| |
| test('keyboard shortcuts with old patch number', () => { |
| element.changeNum = 42 as NumericChangeId; |
| element.patchRange = { |
| basePatchNum: PARENT, |
| patchNum: 1 as RevisionPatchSetNum, |
| }; |
| element.change = { |
| ...createParsedChange(), |
| _number: 42 as NumericChangeId, |
| revisions: { |
| a: createRevision(1), |
| b: createRevision(2), |
| }, |
| }; |
| element.files = getFilesFromFileList([ |
| 'chell.go', |
| 'glados.txt', |
| 'wheatley.md', |
| ]); |
| element.path = 'glados.txt'; |
| |
| pressKey(element, 'u'); |
| assert.isTrue(setUrlStub.calledOnce); |
| assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42/1'); |
| |
| pressKey(element, ']'); |
| assert.equal( |
| setUrlStub.lastCall.firstArg, |
| '/c/test-project/+/42/1/wheatley.md' |
| ); |
| element.path = 'wheatley.md'; |
| |
| pressKey(element, '['); |
| assert.equal( |
| setUrlStub.lastCall.firstArg, |
| '/c/test-project/+/42/1/glados.txt' |
| ); |
| element.path = 'glados.txt'; |
| |
| pressKey(element, '['); |
| assert.equal( |
| setUrlStub.lastCall.firstArg, |
| '/c/test-project/+/42/1/chell.go' |
| ); |
| element.path = 'chell.go'; |
| |
| setUrlStub.reset(); |
| pressKey(element, '['); |
| assert.isTrue(setUrlStub.calledOnce); |
| assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42/1'); |
| }); |
| |
| test('edit should redirect to edit page', async () => { |
| element.loggedIn = true; |
| element.path = 't.txt'; |
| element.patchRange = { |
| basePatchNum: PARENT, |
| patchNum: 1 as RevisionPatchSetNum, |
| }; |
| element.change = { |
| ...createParsedChange(), |
| _number: 42 as NumericChangeId, |
| project: 'gerrit' as RepoName, |
| status: ChangeStatus.NEW, |
| revisions: { |
| a: createRevision(1), |
| b: createRevision(2), |
| }, |
| }; |
| await element.updateComplete; |
| const editBtn = queryAndAssert<GrButton>( |
| element, |
| '.editButton gr-button' |
| ); |
| assert.isTrue(!!editBtn); |
| editBtn.click(); |
| assert.equal(setUrlStub.callCount, 1); |
| assert.equal(setUrlStub.lastCall.firstArg, '/c/gerrit/+/42/1/t.txt,edit'); |
| }); |
| |
| test('edit should redirect to edit page with line number', async () => { |
| const lineNumber = 42; |
| element.loggedIn = true; |
| element.path = 't.txt'; |
| element.patchRange = { |
| basePatchNum: PARENT, |
| patchNum: 1 as RevisionPatchSetNum, |
| }; |
| element.change = { |
| ...createParsedChange(), |
| _number: 42 as NumericChangeId, |
| project: 'gerrit' as RepoName, |
| status: ChangeStatus.NEW, |
| revisions: { |
| a: createRevision(1), |
| b: createRevision(2), |
| }, |
| }; |
| assertIsDefined(element.cursor); |
| sinon |
| .stub(element.cursor, 'getAddress') |
| .returns({number: lineNumber, leftSide: false}); |
| await element.updateComplete; |
| const editBtn = queryAndAssert<GrButton>( |
| element, |
| '.editButton gr-button' |
| ); |
| assert.isTrue(!!editBtn); |
| editBtn.click(); |
| assert.equal(setUrlStub.callCount, 1); |
| assert.equal( |
| setUrlStub.lastCall.firstArg, |
| '/c/gerrit/+/42/1/t.txt,edit#42' |
| ); |
| }); |
| |
| async function isEditVisibile({ |
| loggedIn, |
| changeStatus, |
| }: { |
| loggedIn: boolean; |
| changeStatus: ChangeStatus; |
| }): Promise<boolean> { |
| element.loggedIn = loggedIn; |
| element.path = 't.txt'; |
| element.patchRange = { |
| basePatchNum: PARENT, |
| patchNum: 1 as RevisionPatchSetNum, |
| }; |
| element.change = { |
| ...createParsedChange(), |
| _number: 42 as NumericChangeId, |
| status: changeStatus, |
| revisions: { |
| a: createRevision(1), |
| b: createRevision(2), |
| }, |
| }; |
| await element.updateComplete; |
| const editBtn = query(element, '.editButton gr-button'); |
| return !!editBtn; |
| } |
| |
| test('edit visible only when logged and status NEW', async () => { |
| for (const changeStatus of Object.keys(ChangeStatus) as 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', async () => { |
| const getDiffPrefsContainer = () => |
| query<HTMLSpanElement>(element, '#diffPrefsContainer'); |
| element.prefs = undefined; |
| element.loggedIn = false; |
| await element.updateComplete; |
| assert.isNotOk(getDiffPrefsContainer()); |
| |
| element.loggedIn = true; |
| await element.updateComplete; |
| assert.isNotOk(getDiffPrefsContainer()); |
| |
| element.loggedIn = false; |
| element.prefs = {...createDefaultDiffPrefs(), font_size: 12}; |
| await element.updateComplete; |
| assert.isNotOk(getDiffPrefsContainer()); |
| |
| element.loggedIn = true; |
| element.prefs = {...createDefaultDiffPrefs(), font_size: 12}; |
| await element.updateComplete; |
| assert.isOk(getDiffPrefsContainer()); |
| }); |
| }); |
| |
| test('prefsButton opens gr-diff-preferences', () => { |
| const handlePrefsTapSpy = sinon.spy(element, 'handlePrefsTap'); |
| assertIsDefined(element.diffPreferencesDialog); |
| const overlayOpenStub = sinon.stub(element.diffPreferencesDialog, 'open'); |
| const prefsButton = queryAndAssert<GrButton>(element, '.prefsButton'); |
| prefsButton.click(); |
| |
| assert.isTrue(handlePrefsTapSpy.called); |
| assert.isTrue(overlayOpenStub.called); |
| }); |
| |
| suite('url parameters', () => { |
| setup(() => { |
| sinon.stub(element, 'fetchFiles'); |
| }); |
| |
| test('_formattedFiles', () => { |
| element.changeNum = 42 as NumericChangeId; |
| element.patchRange = { |
| basePatchNum: PARENT, |
| patchNum: 10 as RevisionPatchSetNum, |
| }; |
| element.change = { |
| ...createParsedChange(), |
| _number: 42 as NumericChangeId, |
| }; |
| element.files = getFilesFromFileList([ |
| 'chell.go', |
| 'glados.txt', |
| 'wheatley.md', |
| '/COMMIT_MSG', |
| '/MERGE_LIST', |
| ]); |
| element.path = 'glados.txt'; |
| const expectedFormattedFiles: DropdownItem[] = [ |
| { |
| text: 'chell.go', |
| mobileText: 'chell.go', |
| value: 'chell.go', |
| bottomText: '', |
| file: { |
| ...createFileInfo(), |
| __path: 'chell.go', |
| }, |
| }, |
| { |
| text: 'glados.txt', |
| mobileText: 'glados.txt', |
| value: 'glados.txt', |
| bottomText: '', |
| file: { |
| ...createFileInfo(), |
| __path: 'glados.txt', |
| }, |
| }, |
| { |
| text: 'wheatley.md', |
| mobileText: 'wheatley.md', |
| value: 'wheatley.md', |
| bottomText: '', |
| file: { |
| ...createFileInfo(), |
| __path: 'wheatley.md', |
| }, |
| }, |
| { |
| text: 'Commit message', |
| mobileText: 'Commit message', |
| value: '/COMMIT_MSG', |
| bottomText: '', |
| file: { |
| ...createFileInfo(), |
| __path: '/COMMIT_MSG', |
| }, |
| }, |
| { |
| text: 'Merge list', |
| mobileText: 'Merge list', |
| value: '/MERGE_LIST', |
| bottomText: '', |
| file: { |
| ...createFileInfo(), |
| __path: '/MERGE_LIST', |
| }, |
| }, |
| ]; |
| |
| const result = element.formatFilesForDropdown(); |
| |
| assert.deepEqual(result, expectedFormattedFiles); |
| assert.equal(result[1].value, element.path); |
| }); |
| |
| test('prev/up/next links', async () => { |
| element.changeNum = 42 as NumericChangeId; |
| element.patchRange = { |
| basePatchNum: PARENT, |
| patchNum: 10 as RevisionPatchSetNum, |
| }; |
| element.change = { |
| ...createParsedChange(), |
| _number: 42 as NumericChangeId, |
| revisions: { |
| a: createRevision(10), |
| }, |
| }; |
| element.files = getFilesFromFileList([ |
| 'chell.go', |
| 'glados.txt', |
| 'wheatley.md', |
| ]); |
| element.path = 'glados.txt'; |
| await element.updateComplete; |
| |
| const linkEls = queryAll(element, '.navLink'); |
| assert.equal(linkEls.length, 3); |
| assert.equal( |
| linkEls[0].getAttribute('href'), |
| '/c/test-project/+/42/10/chell.go' |
| ); |
| assert.equal(linkEls[1].getAttribute('href'), '/c/test-project/+/42'); |
| assert.equal( |
| linkEls[2].getAttribute('href'), |
| '/c/test-project/+/42/10/wheatley.md' |
| ); |
| element.path = 'wheatley.md'; |
| await element.updateComplete; |
| assert.equal( |
| linkEls[0].getAttribute('href'), |
| '/c/test-project/+/42/10/glados.txt' |
| ); |
| assert.equal(linkEls[1].getAttribute('href'), '/c/test-project/+/42'); |
| assert.equal(linkEls[2].getAttribute('href'), '/c/test-project/+/42'); |
| element.path = 'chell.go'; |
| await element.updateComplete; |
| assert.equal(linkEls[0].getAttribute('href'), '/c/test-project/+/42'); |
| assert.equal(linkEls[1].getAttribute('href'), '/c/test-project/+/42'); |
| assert.equal( |
| linkEls[2].getAttribute('href'), |
| '/c/test-project/+/42/10/glados.txt' |
| ); |
| element.path = 'not_a_real_file'; |
| await element.updateComplete; |
| assert.equal( |
| linkEls[0].getAttribute('href'), |
| '/c/test-project/+/42/10/wheatley.md' |
| ); |
| assert.equal(linkEls[1].getAttribute('href'), '/c/test-project/+/42'); |
| assert.equal( |
| linkEls[2].getAttribute('href'), |
| '/c/test-project/+/42/10/chell.go' |
| ); |
| }); |
| |
| test('prev/up/next links with patch range', async () => { |
| element.changeNum = 42 as NumericChangeId; |
| element.patchRange = { |
| basePatchNum: 5 as BasePatchSetNum, |
| patchNum: 10 as RevisionPatchSetNum, |
| }; |
| element.change = { |
| ...createParsedChange(), |
| _number: 42 as NumericChangeId, |
| revisions: { |
| a: createRevision(5), |
| b: createRevision(10), |
| }, |
| }; |
| element.files = getFilesFromFileList([ |
| 'chell.go', |
| 'glados.txt', |
| 'wheatley.md', |
| ]); |
| element.path = 'glados.txt'; |
| await element.updateComplete; |
| const linkEls = queryAll(element, '.navLink'); |
| assert.equal(linkEls.length, 3); |
| assert.equal( |
| linkEls[0].getAttribute('href'), |
| '/c/test-project/+/42/5..10/chell.go' |
| ); |
| assert.equal( |
| linkEls[1].getAttribute('href'), |
| '/c/test-project/+/42/5..10' |
| ); |
| assert.equal( |
| linkEls[2].getAttribute('href'), |
| '/c/test-project/+/42/5..10/wheatley.md' |
| ); |
| element.path = 'wheatley.md'; |
| await element.updateComplete; |
| assert.equal( |
| linkEls[0].getAttribute('href'), |
| '/c/test-project/+/42/5..10/glados.txt' |
| ); |
| assert.equal( |
| linkEls[1].getAttribute('href'), |
| '/c/test-project/+/42/5..10' |
| ); |
| assert.equal( |
| linkEls[2].getAttribute('href'), |
| '/c/test-project/+/42/5..10' |
| ); |
| element.path = 'chell.go'; |
| await element.updateComplete; |
| assert.equal( |
| linkEls[0].getAttribute('href'), |
| '/c/test-project/+/42/5..10' |
| ); |
| assert.equal( |
| linkEls[1].getAttribute('href'), |
| '/c/test-project/+/42/5..10' |
| ); |
| assert.equal( |
| linkEls[2].getAttribute('href'), |
| '/c/test-project/+/42/5..10/glados.txt' |
| ); |
| }); |
| }); |
| |
| test('handlePatchChange calls setUrl correctly', async () => { |
| element.change = { |
| ...createParsedChange(), |
| _number: 321 as NumericChangeId, |
| project: 'foo/bar' as RepoName, |
| }; |
| element.path = 'path/to/file.txt'; |
| |
| element.patchRange = { |
| basePatchNum: PARENT, |
| patchNum: 3 as RevisionPatchSetNum, |
| }; |
| await element.updateComplete; |
| |
| const detail = { |
| basePatchNum: PARENT, |
| patchNum: 1 as RevisionPatchSetNum, |
| }; |
| |
| queryAndAssert(element, '#rangeSelect').dispatchEvent( |
| new CustomEvent('patch-range-change', {detail, bubbles: false}) |
| ); |
| |
| assert.equal( |
| setUrlStub.lastCall.firstArg, |
| '/c/foo/bar/+/321/1/path/to/file.txt' |
| ); |
| }); |
| |
| test( |
| '_prefs.manual_review true means set reviewed is not ' + |
| 'automatically called', |
| async () => { |
| const setReviewedFileStatusStub = sinon |
| .stub(element.getChangeModel(), 'setReviewedFilesStatus') |
| .callsFake(() => Promise.resolve()); |
| |
| const setReviewedStatusStub = sinon.spy(element, 'setReviewedStatus'); |
| |
| assertIsDefined(element.diffHost); |
| sinon.stub(element.diffHost, 'reload'); |
| element.loggedIn = true; |
| const diffPreferences = { |
| ...createDefaultDiffPrefs(), |
| manual_review: true, |
| }; |
| element.userModel.setDiffPreferences(diffPreferences); |
| element.getChangeModel().setState({ |
| change: createParsedChange(), |
| diffPath: '/COMMIT_MSG', |
| reviewedFiles: [], |
| loadingStatus: LoadingStatus.LOADED, |
| }); |
| |
| element.routerModel.setState({ |
| changeNum: TEST_NUMERIC_CHANGE_ID, |
| view: GerritView.DIFF, |
| patchNum: 2 as RevisionPatchSetNum, |
| }); |
| element.patchRange = { |
| patchNum: 2 as RevisionPatchSetNum, |
| basePatchNum: 1 as BasePatchSetNum, |
| }; |
| |
| await waitUntil(() => setReviewedStatusStub.called); |
| |
| assert.isFalse(setReviewedFileStatusStub.called); |
| |
| // if prefs are updated then the reviewed status should not be set again |
| element.userModel.setDiffPreferences(createDefaultDiffPrefs()); |
| |
| await element.updateComplete; |
| assert.isFalse(setReviewedFileStatusStub.called); |
| } |
| ); |
| |
| test('_prefs.manual_review false means set reviewed is called', async () => { |
| const setReviewedFileStatusStub = sinon |
| .stub(element.getChangeModel(), 'setReviewedFilesStatus') |
| .callsFake(() => Promise.resolve()); |
| |
| assertIsDefined(element.diffHost); |
| sinon.stub(element.diffHost, 'reload'); |
| element.loggedIn = true; |
| const diffPreferences = { |
| ...createDefaultDiffPrefs(), |
| manual_review: false, |
| }; |
| element.userModel.setDiffPreferences(diffPreferences); |
| element.getChangeModel().setState({ |
| change: createParsedChange(), |
| diffPath: '/COMMIT_MSG', |
| reviewedFiles: [], |
| loadingStatus: LoadingStatus.LOADED, |
| }); |
| |
| element.routerModel.setState({ |
| changeNum: TEST_NUMERIC_CHANGE_ID, |
| view: GerritView.DIFF, |
| patchNum: 22 as RevisionPatchSetNum, |
| }); |
| element.patchRange = { |
| patchNum: 2 as RevisionPatchSetNum, |
| basePatchNum: 1 as BasePatchSetNum, |
| }; |
| |
| await waitUntil(() => setReviewedFileStatusStub.called); |
| |
| assert.isTrue(setReviewedFileStatusStub.called); |
| }); |
| |
| test('file review status', async () => { |
| element.getChangeModel().setState({ |
| change: createParsedChange(), |
| diffPath: '/COMMIT_MSG', |
| reviewedFiles: [], |
| loadingStatus: LoadingStatus.LOADED, |
| }); |
| element.loggedIn = true; |
| const saveReviewedStub = sinon |
| .stub(element.getChangeModel(), 'setReviewedFilesStatus') |
| .callsFake(() => Promise.resolve()); |
| assertIsDefined(element.diffHost); |
| sinon.stub(element.diffHost, 'reload'); |
| |
| element.userModel.setDiffPreferences(createDefaultDiffPrefs()); |
| |
| element.routerModel.setState({ |
| changeNum: TEST_NUMERIC_CHANGE_ID, |
| view: GerritView.DIFF, |
| patchNum: 2 as RevisionPatchSetNum, |
| }); |
| |
| element.patchRange = { |
| patchNum: 2 as RevisionPatchSetNum, |
| basePatchNum: 1 as BasePatchSetNum, |
| }; |
| |
| await waitUntil(() => saveReviewedStub.called); |
| |
| element.getChangeModel().updateStateFileReviewed('/COMMIT_MSG', true); |
| await element.updateComplete; |
| |
| const reviewedStatusCheckBox = queryAndAssert<HTMLInputElement>( |
| element, |
| 'input[type="checkbox"]' |
| ); |
| |
| assert.isTrue(reviewedStatusCheckBox.checked); |
| assert.deepEqual(saveReviewedStub.lastCall.args, [ |
| 42, |
| 2, |
| '/COMMIT_MSG', |
| true, |
| ]); |
| |
| reviewedStatusCheckBox.click(); |
| assert.isFalse(reviewedStatusCheckBox.checked); |
| assert.deepEqual(saveReviewedStub.lastCall.args, [ |
| 42, |
| 2, |
| '/COMMIT_MSG', |
| false, |
| ]); |
| |
| element.getChangeModel().updateStateFileReviewed('/COMMIT_MSG', false); |
| await element.updateComplete; |
| |
| reviewedStatusCheckBox.click(); |
| assert.isTrue(reviewedStatusCheckBox.checked); |
| assert.deepEqual(saveReviewedStub.lastCall.args, [ |
| 42, |
| 2, |
| '/COMMIT_MSG', |
| true, |
| ]); |
| |
| const callCount = saveReviewedStub.callCount; |
| |
| element.viewState = { |
| view: GerritView.DIFF, |
| changeNum: 42 as NumericChangeId, |
| project: 'test' as RepoName, |
| }; |
| await element.updateComplete; |
| |
| // saveReviewedState observer observes viewState, but should not fire when |
| // view !== GerritView.DIFF. |
| assert.equal(saveReviewedStub.callCount, callCount); |
| }); |
| |
| test('file review status with edit loaded', async () => { |
| const saveReviewedStub = sinon.stub( |
| element.getChangeModel(), |
| 'setReviewedFilesStatus' |
| ); |
| |
| element.patchRange = { |
| basePatchNum: 1 as BasePatchSetNum, |
| patchNum: EDIT, |
| }; |
| await waitEventLoop(); |
| |
| assert.isTrue(element.computeEditMode()); |
| element.setReviewed(true); |
| assert.isFalse(saveReviewedStub.called); |
| }); |
| |
| test('hash is determined from viewState', async () => { |
| assertIsDefined(element.diffHost); |
| sinon.stub(element.diffHost, 'reload'); |
| const initLineStub = sinon.stub(element, 'initLineOfInterestAndCursor'); |
| |
| element.loggedIn = true; |
| element.viewState = { |
| view: GerritView.DIFF, |
| changeNum: 42 as NumericChangeId, |
| patchNum: 2 as RevisionPatchSetNum, |
| basePatchNum: 1 as BasePatchSetNum, |
| path: '/COMMIT_MSG', |
| }; |
| |
| await element.updateComplete; |
| await waitEventLoop(); |
| assert.isTrue(initLineStub.calledOnce); |
| }); |
| |
| test('diff mode selector correctly toggles the diff', async () => { |
| const select = queryAndAssert<GrDiffModeSelector>(element, '#modeSelect'); |
| const diffDisplay = element.diffHost; |
| assertIsDefined(diffDisplay); |
| element.userPrefs = { |
| ...createDefaultPreferences(), |
| diff_view: DiffViewMode.SIDE_BY_SIDE, |
| }; |
| element.getBrowserModel().setScreenWidth(0); |
| |
| const userStub = stubUsers('updatePreferences'); |
| |
| await element.updateComplete; |
| // The mode selected in the view state reflects the selected option. |
| // assert.equal(element.userPrefs.diff_view, 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. |
| element.handleToggleDiffMode(); |
| assert.isTrue( |
| userStub.calledWithExactly({ |
| diff_view: DiffViewMode.UNIFIED, |
| }) |
| ); |
| }); |
| |
| test('diff mode selector should be hidden for binary', async () => { |
| element.diff = { |
| ...createDiff(), |
| binary: true, |
| content: [], |
| }; |
| |
| await element.updateComplete; |
| const diffModeSelector = queryAndAssert(element, '.diffModeSelector'); |
| assert.isTrue(diffModeSelector.classList.contains('hide')); |
| }); |
| |
| suite('commitRange', () => { |
| const change: ParsedChangeInfo = { |
| ...createParsedChange(), |
| _number: 42 as NumericChangeId, |
| revisions: { |
| 'commit-sha-1': { |
| ...createRevision(1), |
| commit: { |
| ...createCommit(), |
| parents: [{subject: 's1', commit: 'sha-1-parent' as CommitId}], |
| }, |
| }, |
| 'commit-sha-2': createRevision(2), |
| 'commit-sha-3': createRevision(3), |
| 'commit-sha-4': createRevision(4), |
| 'commit-sha-5': { |
| ...createRevision(5), |
| commit: { |
| ...createCommit(), |
| parents: [{subject: 's5', commit: 'sha-5-parent' as CommitId}], |
| }, |
| }, |
| }, |
| }; |
| setup(async () => { |
| assertIsDefined(element.diffHost); |
| sinon.stub(element.diffHost, 'reload'); |
| sinon.stub(element, 'initCursor'); |
| element.change = change; |
| await element.updateComplete; |
| await element.diffHost.updateComplete; |
| }); |
| |
| test('uses the patchNum and basePatchNum ', async () => { |
| element.viewState = { |
| view: GerritView.DIFF, |
| changeNum: 42 as NumericChangeId, |
| patchNum: 4 as RevisionPatchSetNum, |
| basePatchNum: 2 as BasePatchSetNum, |
| path: '/COMMIT_MSG', |
| }; |
| element.change = change; |
| await element.updateComplete; |
| await waitEventLoop(); |
| assert.deepEqual(element.commitRange, { |
| baseCommit: 'commit-sha-2' as CommitId, |
| commit: 'commit-sha-4' as CommitId, |
| }); |
| }); |
| |
| test('uses the parent when there is no base patch num ', async () => { |
| element.viewState = { |
| view: GerritView.DIFF, |
| changeNum: 42 as NumericChangeId, |
| patchNum: 5 as RevisionPatchSetNum, |
| path: '/COMMIT_MSG', |
| }; |
| element.change = change; |
| await element.updateComplete; |
| await waitEventLoop(); |
| assert.deepEqual(element.commitRange, { |
| commit: 'commit-sha-5' as CommitId, |
| baseCommit: 'sha-5-parent' as CommitId, |
| }); |
| }); |
| }); |
| |
| test('initCursor', () => { |
| assertIsDefined(element.cursor); |
| assert.isNotOk(element.cursor.initialLineNumber); |
| |
| // Does nothing when viewState specify no cursor address: |
| element.initCursor(false); |
| assert.isNotOk(element.cursor.initialLineNumber); |
| |
| // Does nothing when viewState 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, 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, Side.LEFT); |
| |
| // Specifies right side: |
| element.focusLineNum = 123; |
| element.initCursor(false); |
| assert.equal(element.cursor.initialLineNumber, 123); |
| assert.equal(element.cursor.side, Side.RIGHT); |
| }); |
| |
| test('getLineOfInterest', () => { |
| assert.isUndefined(element.getLineOfInterest(false)); |
| |
| element.focusLineNum = 12; |
| let result = element.getLineOfInterest(false); |
| assert.isOk(result); |
| assert.equal(result!.lineNum, 12); |
| assert.equal(result!.side, Side.RIGHT); |
| |
| result = element.getLineOfInterest(true); |
| assert.isOk(result); |
| assert.equal(result!.lineNum, 12); |
| assert.equal(result!.side, Side.LEFT); |
| }); |
| |
| test('onLineSelected', () => { |
| const replaceStateStub = sinon.stub(history, 'replaceState'); |
| assertIsDefined(element.cursor); |
| sinon |
| .stub(element.cursor, 'getAddress') |
| .returns({number: 123, leftSide: false}); |
| |
| element.changeNum = 321 as NumericChangeId; |
| element.change = { |
| ...createParsedChange(), |
| _number: 321 as NumericChangeId, |
| project: 'foo/bar' as RepoName, |
| }; |
| element.patchRange = { |
| basePatchNum: 3 as BasePatchSetNum, |
| patchNum: 5 as RevisionPatchSetNum, |
| }; |
| const e = {detail: {number: 123, side: Side.RIGHT}} as CustomEvent; |
| |
| element.onLineSelected(e); |
| |
| assert.isTrue(replaceStateStub.called); |
| }); |
| |
| test('line selected on left side', () => { |
| const replaceStateStub = sinon.stub(history, 'replaceState'); |
| assertIsDefined(element.cursor); |
| sinon |
| .stub(element.cursor, 'getAddress') |
| .returns({number: 123, leftSide: true}); |
| |
| element.changeNum = 321 as NumericChangeId; |
| element.change = { |
| ...createParsedChange(), |
| _number: 321 as NumericChangeId, |
| project: 'foo/bar' as RepoName, |
| }; |
| element.patchRange = { |
| basePatchNum: 3 as BasePatchSetNum, |
| patchNum: 5 as RevisionPatchSetNum, |
| }; |
| const e = {detail: {number: 123, side: Side.LEFT}} as CustomEvent; |
| |
| element.onLineSelected(e); |
| |
| assert.isTrue(replaceStateStub.called); |
| }); |
| |
| test('handleToggleDiffMode', () => { |
| const userStub = stubUsers('updatePreferences'); |
| element.userPrefs = { |
| ...createDefaultPreferences(), |
| diff_view: DiffViewMode.SIDE_BY_SIDE, |
| }; |
| |
| element.handleToggleDiffMode(); |
| assert.deepEqual(userStub.lastCall.args[0], { |
| diff_view: DiffViewMode.UNIFIED, |
| }); |
| |
| element.userPrefs = { |
| ...createDefaultPreferences(), |
| diff_view: DiffViewMode.UNIFIED, |
| }; |
| |
| element.handleToggleDiffMode(); |
| assert.deepEqual(userStub.lastCall.args[0], { |
| diff_view: DiffViewMode.SIDE_BY_SIDE, |
| }); |
| }); |
| |
| suite('initPatchRange', () => { |
| setup(async () => { |
| getDiffRestApiStub.returns(Promise.resolve(createDiff())); |
| element.viewState = { |
| view: GerritView.DIFF, |
| changeNum: 42 as NumericChangeId, |
| patchNum: 3 as RevisionPatchSetNum, |
| path: 'abcd', |
| }; |
| await element.updateComplete; |
| }); |
| test('empty', () => { |
| sinon.stub(element, 'getPaths').returns({}); |
| element.initPatchRange(); |
| assert.equal(Object.keys(element.commentMap ?? {}).length, 0); |
| }); |
| |
| test('has paths', () => { |
| sinon.stub(element, 'fetchFiles'); |
| sinon.stub(element, 'getPaths').returns({ |
| 'path/to/file/one.cpp': true, |
| 'path-to/file/two.py': true, |
| }); |
| element.changeNum = 42 as NumericChangeId; |
| element.patchRange = { |
| basePatchNum: 3 as BasePatchSetNum, |
| patchNum: 5 as RevisionPatchSetNum, |
| }; |
| 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 result = element.computeCommentSkips(commentMap, [], path); |
| assert.isOk(result); |
| assert.isNotOk(result!.previous); |
| assert.isNotOk(result!.next); |
| }); |
| |
| test('finds skips', () => { |
| const fileList = ['path/one.jpg', 'path/two.m4v', 'path/three.wav']; |
| let path = fileList[1]; |
| const commentMap: CommentMap = {}; |
| commentMap[fileList[0]] = true; |
| commentMap[fileList[1]] = false; |
| commentMap[fileList[2]] = true; |
| |
| let result = element.computeCommentSkips(commentMap, fileList, path); |
| assert.isOk(result); |
| assert.equal(result!.previous, fileList[0]); |
| assert.equal(result!.next, fileList[2]); |
| |
| commentMap[fileList[1]] = true; |
| |
| result = element.computeCommentSkips(commentMap, fileList, path); |
| assert.isOk(result); |
| assert.equal(result!.previous, fileList[0]); |
| assert.equal(result!.next, fileList[2]); |
| |
| path = fileList[0]; |
| |
| result = element.computeCommentSkips(commentMap, fileList, path); |
| assert.isOk(result); |
| assert.isNull(result!.previous); |
| assert.equal(result!.next, fileList[1]); |
| |
| path = fileList[2]; |
| |
| result = element.computeCommentSkips(commentMap, fileList, path); |
| assert.isOk(result); |
| assert.equal(result!.previous, fileList[1]); |
| assert.isNull(result!.next); |
| }); |
| |
| suite('skip next/previous', () => { |
| let navToChangeStub: SinonStub; |
| |
| setup(() => { |
| navToChangeStub = sinon.stub(element, 'navToChangeView'); |
| element.files = getFilesFromFileList([ |
| 'path/one.jpg', |
| 'path/two.m4v', |
| 'path/three.wav', |
| ]); |
| element.patchRange = { |
| patchNum: 2 as RevisionPatchSetNum, |
| basePatchNum: 1 as BasePatchSetNum, |
| }; |
| }); |
| |
| suite('moveToPreviousFileWithComment', () => { |
| test('no skips', () => { |
| element.moveToPreviousFileWithComment(); |
| assert.isFalse(navToChangeStub.called); |
| assert.isFalse(setUrlStub.called); |
| }); |
| |
| test('no previous', async () => { |
| const commentMap: CommentMap = {}; |
| commentMap[element.files.sortedFileList[0]!] = false; |
| commentMap[element.files.sortedFileList[1]!] = false; |
| commentMap[element.files.sortedFileList[2]!] = true; |
| element.commentMap = commentMap; |
| element.path = element.files.sortedFileList[1]; |
| await element.updateComplete; |
| |
| element.moveToPreviousFileWithComment(); |
| assert.isTrue(navToChangeStub.calledOnce); |
| assert.isFalse(setUrlStub.called); |
| }); |
| |
| test('w/ previous', async () => { |
| const commentMap: CommentMap = {}; |
| commentMap[element.files.sortedFileList[0]!] = true; |
| commentMap[element.files.sortedFileList[1]!] = false; |
| commentMap[element.files.sortedFileList[2]!] = true; |
| element.commentMap = commentMap; |
| element.path = element.files.sortedFileList[1]; |
| await element.updateComplete; |
| |
| element.moveToPreviousFileWithComment(); |
| assert.isFalse(navToChangeStub.called); |
| assert.isTrue(setUrlStub.calledOnce); |
| }); |
| }); |
| |
| suite('moveToNextFileWithComment', () => { |
| test('no skips', () => { |
| element.moveToNextFileWithComment(); |
| assert.isFalse(navToChangeStub.called); |
| assert.isFalse(setUrlStub.called); |
| }); |
| |
| test('no previous', async () => { |
| const commentMap: CommentMap = {}; |
| commentMap[element.files.sortedFileList[0]!] = true; |
| commentMap[element.files.sortedFileList[1]!] = false; |
| commentMap[element.files.sortedFileList[2]!] = false; |
| element.commentMap = commentMap; |
| element.path = element.files.sortedFileList[1]; |
| await element.updateComplete; |
| |
| element.moveToNextFileWithComment(); |
| assert.isTrue(navToChangeStub.calledOnce); |
| assert.isFalse(setUrlStub.called); |
| }); |
| |
| test('w/ previous', async () => { |
| const commentMap: CommentMap = {}; |
| commentMap[element.files.sortedFileList[0]!] = true; |
| commentMap[element.files.sortedFileList[1]!] = false; |
| commentMap[element.files.sortedFileList[2]!] = true; |
| element.commentMap = commentMap; |
| element.path = element.files.sortedFileList[1]; |
| await element.updateComplete; |
| |
| element.moveToNextFileWithComment(); |
| assert.isFalse(navToChangeStub.called); |
| assert.isTrue(setUrlStub.calledOnce); |
| }); |
| }); |
| }); |
| }); |
| |
| test('_computeEditMode', () => { |
| const callCompute = (range: PatchRange) => { |
| element.patchRange = range; |
| return element.computeEditMode(); |
| }; |
| assert.isFalse( |
| callCompute({ |
| basePatchNum: PARENT, |
| patchNum: 1 as RevisionPatchSetNum, |
| }) |
| ); |
| assert.isTrue( |
| callCompute({ |
| basePatchNum: 1 as BasePatchSetNum, |
| patchNum: EDIT, |
| }) |
| ); |
| }); |
| |
| test('computeFileNum', () => { |
| element.path = '/foo'; |
| assert.equal( |
| element.computeFileNum([ |
| {text: '/foo', value: '/foo'}, |
| {text: '/bar', value: '/bar'}, |
| ]), |
| 1 |
| ); |
| element.path = '/bar'; |
| assert.equal( |
| element.computeFileNum([ |
| {text: '/foo', value: '/foo'}, |
| {text: '/bar', value: '/bar'}, |
| ]), |
| 2 |
| ); |
| }); |
| |
| test('computeFileNumClass', () => { |
| assert.equal(element.computeFileNumClass(0, []), ''); |
| assert.equal( |
| element.computeFileNumClass(1, [ |
| {text: '/foo', value: '/foo'}, |
| {text: '/bar', value: '/bar'}, |
| ]), |
| 'show' |
| ); |
| }); |
| |
| test('f open file dropdown', async () => { |
| assertIsDefined(element.dropdown); |
| assertIsDefined(element.dropdown.dropdown); |
| assert.isFalse(element.dropdown.dropdown.opened); |
| pressKey(element, 'f'); |
| await element.updateComplete; |
| assert.isTrue(element.dropdown.dropdown.opened); |
| }); |
| |
| suite('blame', () => { |
| test('toggle blame with button', () => { |
| assertIsDefined(element.diffHost); |
| const toggleBlame = sinon |
| .stub(element.diffHost, 'loadBlame') |
| .callsFake(() => Promise.resolve([])); |
| queryAndAssert<GrButton>(element, '#toggleBlame').click(); |
| assert.isTrue(toggleBlame.calledOnce); |
| }); |
| test('toggle blame with shortcut', () => { |
| assertIsDefined(element.diffHost); |
| const toggleBlame = sinon |
| .stub(element.diffHost, 'loadBlame') |
| .callsFake(() => Promise.resolve([])); |
| pressKey(element, 'b'); |
| assert.isTrue(toggleBlame.calledOnce); |
| }); |
| }); |
| |
| suite('editMode behavior', () => { |
| setup(async () => { |
| element.loggedIn = true; |
| await element.updateComplete; |
| }); |
| |
| test('reviewed checkbox', async () => { |
| sinon.stub(element, 'handlePatchChange'); |
| element.patchRange = createPatchRange(); |
| await element.updateComplete; |
| assertIsDefined(element.reviewed); |
| // Reviewed checkbox should be shown. |
| assert.isTrue(isVisible(element.reviewed)); |
| element.patchRange = {...element.patchRange, patchNum: EDIT}; |
| await element.updateComplete; |
| |
| assert.isFalse(isVisible(element.reviewed)); |
| }); |
| }); |
| |
| suite('switching files', () => { |
| let dispatchEventStub: SinonStub; |
| let navToFileStub: SinonStub; |
| let moveToPreviousChunkStub: SinonStub; |
| let moveToNextChunkStub: SinonStub; |
| let isAtStartStub: SinonStub; |
| let isAtEndStub: SinonStub; |
| let nowStub: SinonStub; |
| |
| setup(() => { |
| dispatchEventStub = sinon.stub(element, 'dispatchEvent').callThrough(); |
| navToFileStub = sinon.stub(element, 'navToFile'); |
| assertIsDefined(element.cursor); |
| 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); |
| |
| pressKey(element, 'n'); |
| |
| assert.isTrue(moveToNextChunkStub.called); |
| assert.equal( |
| dispatchEventStub.lastCall.args[0].type, |
| EventType.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); |
| pressKey(element, 'n'); |
| nowStub.returns(10); |
| pressKey(element, 'n'); |
| |
| assert.isTrue(navToFileStub.called); |
| assert.deepEqual(navToFileStub.lastCall.args, [['file1', 'file3'], 1]); |
| }); |
| |
| test('does not navigate if n is tapped twice too slow', () => { |
| moveToNextChunkStub.returns(CursorMoveResult.CLIPPED); |
| isAtEndStub.returns(true); |
| |
| nowStub.returns(5); |
| pressKey(element, 'n'); |
| nowStub.returns(6000); |
| pressKey(element, 'n'); |
| |
| assert.isFalse(navToFileStub.called); |
| }); |
| |
| test('shows toast when at the start of file', () => { |
| moveToPreviousChunkStub.returns(CursorMoveResult.CLIPPED); |
| isAtStartStub.returns(true); |
| |
| pressKey(element, 'p'); |
| |
| assert.isTrue(moveToPreviousChunkStub.called); |
| assert.equal( |
| dispatchEventStub.lastCall.args[0].type, |
| EventType.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); |
| pressKey(element, 'p'); |
| nowStub.returns(10); |
| pressKey(element, 'p'); |
| |
| assert.isTrue(navToFileStub.called); |
| assert.deepEqual(navToFileStub.lastCall.args, [['file1', 'file3'], -1]); |
| }); |
| |
| test('does not navigate if p is tapped twice too slow', () => { |
| moveToPreviousChunkStub.returns(CursorMoveResult.CLIPPED); |
| isAtStartStub.returns(true); |
| |
| nowStub.returns(5); |
| pressKey(element, 'p'); |
| nowStub.returns(6000); |
| pressKey(element, 'p'); |
| |
| assert.isFalse(navToFileStub.called); |
| }); |
| |
| test('does not navigate when tapping n then p', () => { |
| moveToNextChunkStub.returns(CursorMoveResult.CLIPPED); |
| isAtEndStub.returns(true); |
| |
| nowStub.returns(5); |
| pressKey(element, 'n'); |
| |
| moveToPreviousChunkStub.returns(CursorMoveResult.CLIPPED); |
| isAtStartStub.returns(true); |
| |
| nowStub.returns(10); |
| pressKey(element, 'p'); |
| |
| assert.isFalse(navToFileStub.called); |
| }); |
| }); |
| |
| test('shift+m navigates to next unreviewed file', async () => { |
| 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'); |
| pressKey(element, 'M'); |
| await waitEventLoop(); |
| |
| assert.isTrue(reviewedStub.lastCall.args[0]); |
| assert.deepEqual(navStub.lastCall.args, [['file1', 'file3'], 1]); |
| }); |
| |
| test('File change should trigger setUrl once', async () => { |
| element.files = getFilesFromFileList(['file1', 'file2', 'file3']); |
| sinon.stub(element, 'initLineOfInterestAndCursor'); |
| |
| // Load file1 |
| element.viewState = { |
| view: GerritView.DIFF, |
| patchNum: 1 as RevisionPatchSetNum, |
| changeNum: 101 as NumericChangeId, |
| project: 'test-project' as RepoName, |
| path: 'file1', |
| }; |
| element.patchRange = { |
| patchNum: 1 as RevisionPatchSetNum, |
| basePatchNum: PARENT, |
| }; |
| element.change = { |
| ...createParsedChange(), |
| revisions: createRevisions(1), |
| }; |
| await element.updateComplete; |
| assert.isFalse(setUrlStub.called); |
| |
| // Switch to file2 |
| element.handleFileChange( |
| new CustomEvent('value-change', {detail: {value: 'file2'}}) |
| ); |
| assert.isTrue(setUrlStub.calledOnce); |
| |
| // This is to mock the param change triggered by above navigate |
| element.viewState = { |
| view: GerritView.DIFF, |
| patchNum: 1 as RevisionPatchSetNum, |
| changeNum: 101 as NumericChangeId, |
| project: 'test-project' as RepoName, |
| path: 'file2', |
| }; |
| element.patchRange = { |
| patchNum: 1 as RevisionPatchSetNum, |
| basePatchNum: PARENT, |
| }; |
| |
| // No extra call |
| assert.isTrue(setUrlStub.calledOnce); |
| }); |
| |
| 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', |
| }, |
| ]; |
| |
| element.change = createParsedChange(); |
| element.change.project = 'test' as RepoName; |
| element.changeNum = 12 as NumericChangeId; |
| element.patchRange = { |
| patchNum: 1 as RevisionPatchSetNum, |
| basePatchNum: PARENT, |
| }; |
| element.path = 'index.php'; |
| element.diff = createDiff(); |
| assert.deepEqual(element.computeDownloadDropdownLinks(), 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 diff = createDiff(); |
| diff.change_type = 'RENAMED'; |
| diff.meta_a!.name = 'index2.php'; |
| |
| element.change = createParsedChange(); |
| element.change.project = 'test' as RepoName; |
| element.changeNum = 12 as NumericChangeId; |
| element.patchRange = { |
| patchNum: 3 as RevisionPatchSetNum, |
| basePatchNum: 2 as BasePatchSetNum, |
| }; |
| element.path = 'index.php'; |
| element.diff = diff; |
| assert.deepEqual(element.computeDownloadDropdownLinks(), downloadLinks); |
| }); |
| |
| test('computeDownloadFileLink', () => { |
| assert.equal( |
| element.computeDownloadFileLink( |
| 'test' as RepoName, |
| 12 as NumericChangeId, |
| {patchNum: 1 as PatchSetNumber, basePatchNum: PARENT}, |
| 'index.php', |
| true |
| ), |
| '/changes/test~12/revisions/1/files/index.php/download?parent=1' |
| ); |
| |
| assert.equal( |
| element.computeDownloadFileLink( |
| 'test' as RepoName, |
| 12 as NumericChangeId, |
| {patchNum: 1 as PatchSetNumber, basePatchNum: -2 as PatchSetNumber}, |
| 'index.php', |
| true |
| ), |
| '/changes/test~12/revisions/1/files/index.php/download?parent=2' |
| ); |
| |
| assert.equal( |
| element.computeDownloadFileLink( |
| 'test' as RepoName, |
| 12 as NumericChangeId, |
| {patchNum: 3 as PatchSetNumber, basePatchNum: 2 as PatchSetNumber}, |
| 'index.php', |
| true |
| ), |
| '/changes/test~12/revisions/2/files/index.php/download' |
| ); |
| |
| assert.equal( |
| element.computeDownloadFileLink( |
| 'test' as RepoName, |
| 12 as NumericChangeId, |
| {patchNum: 3 as PatchSetNumber, basePatchNum: 2 as PatchSetNumber}, |
| 'index.php', |
| false |
| ), |
| '/changes/test~12/revisions/3/files/index.php/download' |
| ); |
| }); |
| |
| test('computeDownloadPatchLink', () => { |
| assert.equal( |
| element.computeDownloadPatchLink( |
| 'test' as RepoName, |
| 12 as NumericChangeId, |
| {basePatchNum: PARENT, patchNum: 1 as RevisionPatchSetNum}, |
| 'index.php' |
| ), |
| '/changes/test~12/revisions/1/patch?zip&path=index.php' |
| ); |
| }); |
| }); |
| |
| suite('unmodified files with comments', () => { |
| let element: GrDiffView; |
| |
| setup(async () => { |
| const changedFiles = { |
| 'file1.txt': createFileInfo(), |
| 'a/b/test.c': createFileInfo(), |
| }; |
| stubRestApi('getConfig').returns(Promise.resolve(createServerInfo())); |
| stubRestApi('getProjectConfig').returns(Promise.resolve(createConfig())); |
| stubRestApi('getChangeFiles').returns(Promise.resolve(changedFiles)); |
| stubRestApi('saveFileReviewed').returns(Promise.resolve(new Response())); |
| stubRestApi('getDiffComments').returns(Promise.resolve({})); |
| stubRestApi('getDiffRobotComments').returns(Promise.resolve({})); |
| stubRestApi('getDiffDrafts').returns(Promise.resolve({})); |
| stubRestApi('getReviewedFiles').returns(Promise.resolve([])); |
| element = await fixture(html`<gr-diff-view></gr-diff-view>`); |
| element.changeNum = 42 as NumericChangeId; |
| }); |
| |
| test('fetchFiles add files with comments without changes', () => { |
| element.patchRange = { |
| basePatchNum: 5 as BasePatchSetNum, |
| patchNum: 10 as RevisionPatchSetNum, |
| }; |
| element.changeComments = { |
| getPaths: sinon.stub().returns({ |
| 'file2.txt': {}, |
| 'file1.txt': {}, |
| }), |
| } as unknown as ChangeComments; |
| element.changeNum = 23 as NumericChangeId; |
| return element.fetchFiles().then(() => { |
| assert.deepEqual(element.files, { |
| sortedFileList: ['a/b/test.c', 'file1.txt', 'file2.txt'], |
| changeFilesByPath: { |
| 'file1.txt': createFileInfo(), |
| 'file2.txt': {status: 'U'} as FileInfo, |
| 'a/b/test.c': createFileInfo(), |
| }, |
| }); |
| }); |
| }); |
| }); |
| }); |