| /** |
| * @license |
| * Copyright 2015 Google LLC |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| import '../../../test/common-test-setup'; |
| import './gr-diff-view'; |
| import { |
| ChangeStatus, |
| DiffViewMode, |
| createDefaultDiffPrefs, |
| createDefaultPreferences, |
| } from '../../../constants/constants'; |
| import { |
| isVisible, |
| pressKey, |
| query, |
| queryAll, |
| queryAndAssert, |
| stubRestApi, |
| waitEventLoop, |
| waitUntil, |
| } from '../../../test/test-utils'; |
| import {ChangeComments} from '../gr-comment-api/gr-comment-api'; |
| import { |
| createRevisions, |
| createComment as createCommentGeneric, |
| createDiff, |
| createServerInfo, |
| createConfig, |
| createParsedChange, |
| createRevision, |
| createFileInfo, |
| createDiffViewState, |
| TEST_NUMERIC_CHANGE_ID, |
| } from '../../../test/test-data-generators'; |
| import { |
| BasePatchSetNum, |
| CommentInfo, |
| EDIT, |
| NumericChangeId, |
| PARENT, |
| PatchSetNum, |
| PatchSetNumber, |
| RepoName, |
| RevisionPatchSetNum, |
| UrlEncodedCommentId, |
| } from '../../../types/common'; |
| import {CursorMoveResult} from '../../../api/core'; |
| import {Side} from '../../../api/diff'; |
| import {Files, GrDiffView} from './gr-diff-view'; |
| import {DropdownItem} from '../../shared/gr-dropdown-list/gr-dropdown-list'; |
| import {SinonFakeTimers, SinonStub, SinonStubbedMember} from 'sinon'; |
| import { |
| changeModelToken, |
| ChangeModel, |
| } from '../../../models/change/change-model'; |
| import {assertIsDefined} from '../../../utils/common-util'; |
| import {GrDiffModeSelector} from '../gr-diff-mode-selector/gr-diff-mode-selector'; |
| import {fixture, html, assert} from '@open-wc/testing'; |
| import {GrButton} from '../../shared/gr-button/gr-button'; |
| import {testResolver} from '../../../test/common-test-setup'; |
| import {UserModel, userModelToken} from '../../../models/user/user-model'; |
| import { |
| commentsModelToken, |
| CommentsModel, |
| } from '../../../models/comments/comments-model'; |
| import { |
| BrowserModel, |
| browserModelToken, |
| } from '../../../models/browser/browser-model'; |
| import { |
| ChangeViewModel, |
| changeViewModelToken, |
| } from '../../../models/views/change'; |
| import {FileNameToNormalizedFileInfoMap} from '../../../models/change/files-model'; |
| import {RestApiService} from '../../../services/gr-rest-api/gr-rest-api'; |
| import {GrDiffCursor} from '../../../embed/diff/gr-diff-cursor/gr-diff-cursor'; |
| import {LoadingStatus} from '../../../types/types'; |
| |
| 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: SinonStubbedMember<RestApiService['getDiff']>; |
| let navToChangeStub: SinonStubbedMember<ChangeModel['navigateToChange']>; |
| let navToDiffStub: SinonStubbedMember<ChangeModel['navigateToDiff']>; |
| let navToEditStub: SinonStubbedMember<ChangeModel['navigateToEdit']>; |
| let changeModel: ChangeModel; |
| let viewModel: ChangeViewModel; |
| let commentsModel: CommentsModel; |
| let browserModel: BrowserModel; |
| let userModel: UserModel; |
| |
| function getFilesFromFileList(fileList: string[]): Files { |
| const changeFilesByPath = fileList.reduce((files, path) => { |
| files[path] = createFileInfo(path); |
| return files; |
| }, {} as FileNameToNormalizedFileInfoMap); |
| return { |
| sortedPaths: fileList, |
| changeFilesByPath, |
| }; |
| } |
| |
| setup(async () => { |
| 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({})); |
| |
| viewModel = testResolver(changeViewModelToken); |
| viewModel.setState(createDiffViewState()); |
| element = await fixture(html`<gr-diff-view></gr-diff-view>`); |
| await waitUntil(() => element.changeNum === TEST_NUMERIC_CHANGE_ID); |
| 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.patchNum = 1 as RevisionPatchSetNum; |
| element.basePatchNum = PARENT; |
| element.changeComments = new ChangeComments({ |
| '/COMMIT_MSG': [ |
| createComment('c1', 10, 2, '/COMMIT_MSG'), |
| createComment('c3', 10, PARENT, '/COMMIT_MSG'), |
| ], |
| }); |
| await element.updateComplete; |
| commentsModel = testResolver(commentsModelToken); |
| changeModel = testResolver(changeModelToken); |
| browserModel = testResolver(browserModelToken); |
| userModel = testResolver(userModelToken); |
| navToChangeStub = sinon.stub(changeModel, 'navigateToChange'); |
| navToDiffStub = sinon.stub(changeModel, 'navigateToDiff'); |
| navToEditStub = sinon.stub(changeModel, 'navigateToEdit'); |
| |
| commentsModel.setState({ |
| comments: {}, |
| robotComments: {}, |
| drafts: {}, |
| portedComments: {}, |
| portedDrafts: {}, |
| discardedDrafts: [], |
| }); |
| }); |
| |
| teardown(() => { |
| clock && clock.restore(); |
| sinon.restore(); |
| }); |
| |
| 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 () => { |
| browserModel.setScreenWidth(0); |
| const patchNum = 10 as RevisionPatchSetNum; |
| element.patchNum = patchNum; |
| element.basePatchNum = PARENT; |
| const change = { |
| ...createParsedChange(), |
| _number: 42 as NumericChangeId, |
| revisions: { |
| a: createRevision(patchNum), |
| }, |
| }; |
| changeModel.updateStateChange(change); |
| viewModel.updateState({patchNum}); |
| 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"> |
| <div class="sidebarTriggerContainer"> |
| <gr-endpoint-decorator name="sidebarTrigger"> |
| <gr-endpoint-param name="onTrigger"></gr-endpoint-param> |
| <gr-endpoint-param name="openSidebar"></gr-endpoint-param> |
| </gr-endpoint-decorator> |
| </div> |
| <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 class="sidebarAnchor"></div> |
| </div> |
| <h2 class="assistive-tech-only">Diff view</h2> |
| <div class="diffContainer"> |
| <gr-diff-host id="diffHost"> </gr-diff-host> |
| </div> |
| <gr-apply-fix-dialog id="applyFixDialog"> </gr-apply-fix-dialog> |
| <gr-diff-preferences-dialog id="diffPreferencesDialog"> |
| </gr-diff-preferences-dialog> |
| <dialog id="downloadModal" tabindex="-1"> |
| <gr-download-dialog id="downloadDialog" role="dialog"> |
| </gr-download-dialog> |
| </dialog> |
| ` |
| ); |
| }); |
| |
| test('keyboard shortcuts', async () => { |
| clock = sinon.useFakeTimers(); |
| element.changeNum = 42 as NumericChangeId; |
| browserModel.setScreenWidth(0); |
| element.patchNum = 10 as RevisionPatchSetNum; |
| element.basePatchNum = PARENT; |
| 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; |
| navToChangeStub.reset(); |
| |
| pressKey(element, 'u'); |
| assert.isTrue(navToChangeStub.calledOnce); |
| await element.updateComplete; |
| |
| pressKey(element, ']'); |
| assert.equal(navToDiffStub.callCount, 1); |
| assert.deepEqual(navToDiffStub.lastCall.args, [ |
| {path: 'wheatley.md', lineNum: undefined}, |
| ]); |
| |
| element.path = 'wheatley.md'; |
| await element.updateComplete; |
| |
| pressKey(element, '['); |
| assert.equal(navToDiffStub.callCount, 2); |
| assert.deepEqual(navToDiffStub.lastCall.args, [ |
| {path: 'glados.txt', lineNum: undefined}, |
| ]); |
| |
| element.path = 'glados.txt'; |
| await element.updateComplete; |
| |
| pressKey(element, '['); |
| assert.equal(navToDiffStub.callCount, 3); |
| assert.deepEqual(navToDiffStub.lastCall.args, [ |
| {path: 'chell.go', lineNum: undefined}, |
| ]); |
| |
| element.path = 'chell.go'; |
| await element.updateComplete; |
| |
| pressKey(element, '['); |
| assert.equal(navToChangeStub.callCount, 2); |
| await element.updateComplete; |
| |
| 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 |
| ); |
| |
| const setReviewedStub = sinon.stub(element, 'setReviewed'); |
| const handleToggleSpy = sinon.spy(element, 'handleToggleFileReviewed'); |
| 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: {[path: string]: CommentInfo[]} = { |
| 'wheatley.md': [createComment('c2', 21, 10, 'wheatley.md')], |
| }; |
| element.changeComments = new ChangeComments(comment); |
| element.patchNum = 10 as RevisionPatchSetNum; |
| element.basePatchNum = PARENT; |
| 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; |
| navToDiffStub.reset(); |
| |
| pressKey(element, 'N'); |
| await element.updateComplete; |
| assert.equal(navToDiffStub.callCount, 1); |
| assert.deepEqual(navToDiffStub.lastCall.args, [ |
| {path: 'wheatley.md', lineNum: 21}, |
| ]); |
| |
| element.path = 'wheatley.md'; // navigated to next file |
| |
| pressKey(element, 'N'); |
| await element.updateComplete; |
| |
| assert.equal(navToChangeStub.callCount, 1); |
| }); |
| |
| 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.patchNum = 10 as RevisionPatchSetNum; |
| element.basePatchNum = 5 as BasePatchSetNum; |
| await element.updateComplete; |
| element.handleDiffAgainstBase(); |
| assert.deepEqual(navToDiffStub.lastCall.args, [ |
| {path: 'some/path.txt'}, |
| 10 as RevisionPatchSetNum, |
| PARENT, |
| ]); |
| }); |
| |
| test('diff against latest', async () => { |
| element.path = 'foo'; |
| element.latestPatchNum = 12 as PatchSetNumber; |
| element.patchNum = 10 as RevisionPatchSetNum; |
| element.basePatchNum = 5 as BasePatchSetNum; |
| await element.updateComplete; |
| element.handleDiffAgainstLatest(); |
| assert.deepEqual(navToDiffStub.lastCall.args, [ |
| {path: 'foo'}, |
| 12 as RevisionPatchSetNum, |
| 5 as BasePatchSetNum, |
| ]); |
| }); |
| |
| test('handleDiffBaseAgainstLeft', async () => { |
| element.path = 'foo'; |
| element.latestPatchNum = 10 as PatchSetNumber; |
| element.patchNum = 3 as RevisionPatchSetNum; |
| element.basePatchNum = 1 as BasePatchSetNum; |
| viewModel.setState({ |
| ...createDiffViewState(), |
| patchNum: 3 as RevisionPatchSetNum, |
| basePatchNum: 1 as BasePatchSetNum, |
| diffView: {path: 'foo'}, |
| }); |
| await element.updateComplete; |
| element.handleDiffBaseAgainstLeft(); |
| assert.deepEqual(navToDiffStub.lastCall.args, [ |
| {path: 'foo'}, |
| 1 as RevisionPatchSetNum, |
| PARENT, |
| ]); |
| }); |
| |
| test('handleDiffRightAgainstLatest', async () => { |
| element.path = 'foo'; |
| element.latestPatchNum = 10 as PatchSetNumber; |
| element.patchNum = 3 as RevisionPatchSetNum; |
| element.basePatchNum = 1 as BasePatchSetNum; |
| await element.updateComplete; |
| element.handleDiffRightAgainstLatest(); |
| assert.deepEqual(navToDiffStub.lastCall.args, [ |
| {path: 'foo'}, |
| 10 as RevisionPatchSetNum, |
| 3 as BasePatchSetNum, |
| ]); |
| }); |
| |
| test('handleDiffBaseAgainstLatest', async () => { |
| element.latestPatchNum = 10 as PatchSetNumber; |
| element.patchNum = 3 as RevisionPatchSetNum; |
| element.basePatchNum = 1 as BasePatchSetNum; |
| await element.updateComplete; |
| element.handleDiffBaseAgainstLatest(); |
| assert.deepEqual(navToDiffStub.lastCall.args, [ |
| {path: 'some/path.txt'}, |
| 10 as RevisionPatchSetNum, |
| PARENT, |
| ]); |
| }); |
| |
| 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(navToDiffStub.calledOnce); |
| assert.isTrue(loggedInErrorSpy.called); |
| }); |
| |
| test('A navigates to change with logged in', async () => { |
| element.changeNum = 42 as NumericChangeId; |
| element.patchNum = 10 as RevisionPatchSetNum; |
| element.basePatchNum = 5 as BasePatchSetNum; |
| 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); |
| navToDiffStub.reset(); |
| |
| pressKey(element, 'a'); |
| |
| await element.updateComplete; |
| assert.isTrue(navToChangeStub.calledOnce); |
| assert.deepEqual(navToChangeStub.lastCall.args, [true]); |
| assert.isFalse(loggedInErrorSpy.called); |
| }); |
| |
| test('A navigates to change with old patch number with logged in', async () => { |
| element.changeNum = 42 as NumericChangeId; |
| element.patchNum = 1 as RevisionPatchSetNum; |
| element.basePatchNum = PARENT; |
| 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(navToChangeStub.calledOnce); |
| assert.deepEqual(navToChangeStub.lastCall.args, [true]); |
| assert.isFalse(loggedInErrorSpy.called); |
| }); |
| |
| test('keyboard shortcuts with patch range', () => { |
| element.changeNum = 42 as NumericChangeId; |
| element.patchNum = 10 as RevisionPatchSetNum; |
| element.basePatchNum = 5 as BasePatchSetNum; |
| 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(navToChangeStub.callCount, 1); |
| |
| pressKey(element, ']'); |
| assert.equal(navToDiffStub.callCount, 1); |
| assert.deepEqual(navToDiffStub.lastCall.args, [ |
| {path: 'wheatley.md', lineNum: undefined}, |
| ]); |
| element.path = 'wheatley.md'; |
| |
| pressKey(element, '['); |
| assert.equal(navToDiffStub.callCount, 2); |
| assert.deepEqual(navToDiffStub.lastCall.args, [ |
| {path: 'glados.txt', lineNum: undefined}, |
| ]); |
| element.path = 'glados.txt'; |
| |
| pressKey(element, '['); |
| assert.equal(navToDiffStub.callCount, 3); |
| assert.deepEqual(navToDiffStub.lastCall.args, [ |
| {path: 'chell.go', lineNum: undefined}, |
| ]); |
| element.path = 'chell.go'; |
| |
| pressKey(element, '['); |
| assert.equal(navToChangeStub.callCount, 2); |
| |
| assertIsDefined(element.downloadModal); |
| const downloadModalStub = sinon.stub(element.downloadModal, 'showModal'); |
| pressKey(element, 'd'); |
| assert.isTrue(downloadModalStub.called); |
| }); |
| |
| test('keyboard shortcuts with old patch number', async () => { |
| element.changeNum = 42 as NumericChangeId; |
| element.patchNum = 1 as RevisionPatchSetNum; |
| element.basePatchNum = PARENT; |
| 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(navToChangeStub.calledOnce); |
| |
| pressKey(element, ']'); |
| assert.deepEqual(navToDiffStub.lastCall.args, [ |
| {path: 'wheatley.md', lineNum: undefined}, |
| ]); |
| element.path = 'wheatley.md'; |
| |
| pressKey(element, '['); |
| assert.deepEqual(navToDiffStub.lastCall.args, [ |
| {path: 'glados.txt', lineNum: undefined}, |
| ]); |
| element.path = 'glados.txt'; |
| |
| pressKey(element, '['); |
| assert.deepEqual(navToDiffStub.lastCall.args, [ |
| {path: 'chell.go', lineNum: undefined}, |
| ]); |
| |
| element.path = 'chell.go'; |
| await element.updateComplete; |
| navToDiffStub.reset(); |
| pressKey(element, '['); |
| assert.equal(navToChangeStub.callCount, 2); |
| }); |
| |
| test('reloadDiff is called when patchNum changes', async () => { |
| const reloadStub = sinon.stub(element, 'reloadDiff'); |
| element.patchNum = 5 as RevisionPatchSetNum; |
| await element.updateComplete; |
| assert.isTrue(reloadStub.called); |
| }); |
| |
| test('initializePositions is called when view becomes active', async () => { |
| const reloadStub = sinon.stub(element, 'reloadDiff'); |
| const initializeStub = sinon.stub(element, 'initializePositions'); |
| |
| element.isActiveChildView = false; |
| await element.updateComplete; |
| element.isActiveChildView = true; |
| await element.updateComplete; |
| |
| assert.isTrue(initializeStub.calledOnce); |
| assert.isFalse(reloadStub.called); |
| }); |
| |
| test('edit should redirect to edit page', async () => { |
| element.loggedIn = true; |
| element.path = 't.txt'; |
| element.patchNum = 1 as RevisionPatchSetNum; |
| element.basePatchNum = PARENT; |
| await element.updateComplete; |
| const editBtn = queryAndAssert<GrButton>( |
| element, |
| '.editButton gr-button' |
| ); |
| assert.isTrue(!!editBtn); |
| editBtn.click(); |
| assert.equal(navToEditStub.callCount, 1); |
| assert.deepEqual(navToEditStub.lastCall.args, [ |
| {path: 't.txt', lineNum: undefined}, |
| ]); |
| }); |
| |
| test('edit should redirect to edit page with line number', async () => { |
| const lineNumber = 42; |
| element.loggedIn = true; |
| element.path = 't.txt'; |
| element.patchNum = 1 as RevisionPatchSetNum; |
| element.basePatchNum = PARENT; |
| assertIsDefined(element.cursor); |
| sinon.stub(element.cursor, 'getTargetLineNumber').returns(lineNumber); |
| await element.updateComplete; |
| const editBtn = queryAndAssert<GrButton>( |
| element, |
| '.editButton gr-button' |
| ); |
| assert.isTrue(!!editBtn); |
| editBtn.click(); |
| assert.equal(navToEditStub.callCount, 1); |
| assert.deepEqual(navToEditStub.lastCall.args, [ |
| {path: 't.txt', lineNum: lineNumber}, |
| ]); |
| }); |
| |
| async function isEditVisibile({ |
| loggedIn, |
| changeStatus, |
| }: { |
| loggedIn: boolean; |
| changeStatus: ChangeStatus; |
| }): Promise<boolean> { |
| element.loggedIn = loggedIn; |
| element.path = 't.txt'; |
| element.patchNum = 1 as RevisionPatchSetNum; |
| element.basePatchNum = PARENT; |
| 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', () => { |
| test('_formattedFiles', () => { |
| element.changeNum = 42 as NumericChangeId; |
| element.patchNum = 10 as RevisionPatchSetNum; |
| element.basePatchNum = PARENT; |
| 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 () => { |
| const patchNum = 10 as RevisionPatchSetNum; |
| viewModel.setState({ |
| ...createDiffViewState(), |
| patchNum, |
| }); |
| const change = { |
| ...createParsedChange(), |
| _number: 42 as NumericChangeId, |
| revisions: { |
| a: createRevision(patchNum), |
| }, |
| }; |
| changeModel.updateStateChange(change); |
| await element.updateComplete; |
| |
| 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 () => { |
| viewModel.setState({ |
| ...createDiffViewState(), |
| basePatchNum: 5 as BasePatchSetNum, |
| patchNum: 10 as RevisionPatchSetNum, |
| diffView: {path: 'glados.txt'}, |
| }); |
| const change = { |
| ...createParsedChange(), |
| _number: 42 as NumericChangeId, |
| revisions: { |
| a: createRevision(5), |
| b: createRevision(10), |
| c: createRevision(12), |
| }, |
| }; |
| changeModel.updateStateChange(change); |
| element.files = getFilesFromFileList([ |
| 'chell.go', |
| 'glados.txt', |
| 'wheatley.md', |
| ]); |
| await waitUntil(() => element.path === 'glados.txt'); |
| await waitUntil(() => element.patchRange?.patchNum === 10); |
| |
| 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' |
| ); |
| |
| viewModel.updateState({diffView: {path: 'wheatley.md'}}); |
| await waitUntil(() => element.path === 'wheatley.md'); |
| |
| 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' |
| ); |
| |
| viewModel.updateState({diffView: {path: 'chell.go'}}); |
| await waitUntil(() => element.path === 'chell.go'); |
| |
| 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.path = 'path/to/file.txt'; |
| element.patchNum = 3 as RevisionPatchSetNum; |
| element.basePatchNum = PARENT; |
| await element.updateComplete; |
| |
| const detail = { |
| basePatchNum: PARENT, |
| patchNum: 1 as RevisionPatchSetNum, |
| }; |
| queryAndAssert(element, '#rangeSelect').dispatchEvent( |
| new CustomEvent('patch-range-change', {detail, bubbles: false}) |
| ); |
| |
| assert.deepEqual(navToDiffStub.lastCall.args, [ |
| {path: element.path}, |
| detail.patchNum, |
| detail.basePatchNum, |
| ]); |
| }); |
| |
| test( |
| 'prefs.manual_review true means set reviewed is not ' + |
| 'automatically called', |
| async () => { |
| const setReviewedFileStatusStub = sinon |
| .stub(changeModel, '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, |
| }; |
| userModel.setDiffPreferences(diffPreferences); |
| viewModel.updateState({diffView: {path: 'wheatley.md'}}); |
| changeModel.setState({ |
| change: createParsedChange(), |
| reviewedFiles: [], |
| loadingStatus: LoadingStatus.LOADED, |
| }); |
| |
| await waitUntil(() => setReviewedStatusStub.called); |
| |
| assert.isFalse(setReviewedFileStatusStub.called); |
| |
| // if prefs are updated then the reviewed status should not be set again |
| 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(changeModel, 'setReviewedFilesStatus') |
| .callsFake(() => Promise.resolve()); |
| |
| assertIsDefined(element.diffHost); |
| sinon.stub(element.diffHost, 'reload'); |
| element.loggedIn = true; |
| const diffPreferences = { |
| ...createDefaultDiffPrefs(), |
| manual_review: false, |
| }; |
| userModel.setDiffPreferences(diffPreferences); |
| viewModel.updateState({diffView: {path: 'wheatley.md'}}); |
| changeModel.setState({ |
| change: createParsedChange(), |
| reviewedFiles: [], |
| loadingStatus: LoadingStatus.LOADED, |
| }); |
| |
| await waitUntil(() => setReviewedFileStatusStub.called); |
| |
| assert.isTrue(setReviewedFileStatusStub.called); |
| }); |
| |
| test('file review status', async () => { |
| const saveReviewedStub = sinon |
| .stub(changeModel, 'setReviewedFilesStatus') |
| .callsFake(() => Promise.resolve()); |
| userModel.setDiffPreferences(createDefaultDiffPrefs()); |
| viewModel.updateState({ |
| patchNum: 1 as RevisionPatchSetNum, |
| basePatchNum: PARENT, |
| diffView: {path: '/COMMIT_MSG'}, |
| }); |
| changeModel.setState({ |
| change: createParsedChange(), |
| reviewedFiles: [], |
| loadingStatus: LoadingStatus.LOADED, |
| }); |
| element.loggedIn = true; |
| await waitUntil(() => element.patchRange?.patchNum === 1); |
| await element.updateComplete; |
| assertIsDefined(element.diffHost); |
| sinon.stub(element.diffHost, 'reload'); |
| |
| await waitUntil(() => saveReviewedStub.called); |
| |
| changeModel.updateStateFileReviewed('/COMMIT_MSG', true); |
| await element.updateComplete; |
| |
| const reviewedStatusCheckBox = queryAndAssert<HTMLInputElement>( |
| element, |
| 'input#reviewed' |
| ); |
| |
| assert.isTrue(reviewedStatusCheckBox.checked); |
| assert.deepEqual(saveReviewedStub.lastCall.args, [ |
| 42, |
| 1, |
| '/COMMIT_MSG', |
| true, |
| ]); |
| |
| reviewedStatusCheckBox.click(); |
| assert.isFalse(reviewedStatusCheckBox.checked); |
| assert.deepEqual(saveReviewedStub.lastCall.args, [ |
| 42, |
| 1, |
| '/COMMIT_MSG', |
| false, |
| ]); |
| |
| changeModel.updateStateFileReviewed('/COMMIT_MSG', false); |
| await element.updateComplete; |
| |
| reviewedStatusCheckBox.click(); |
| assert.isTrue(reviewedStatusCheckBox.checked); |
| assert.deepEqual(saveReviewedStub.lastCall.args, [ |
| 42, |
| 1, |
| '/COMMIT_MSG', |
| true, |
| ]); |
| |
| const callCount = saveReviewedStub.callCount; |
| |
| viewModel.setState({ |
| ...createDiffViewState(), |
| repo: 'test' as RepoName, |
| }); |
| await element.updateComplete; |
| |
| // saveReviewedState observer observes viewState, but should not fire when |
| // view !== GerritView.DIFF. |
| assert.equal(saveReviewedStub.callCount, callCount); |
| }); |
| |
| test('do not set file review status for EDIT patchset', async () => { |
| const saveReviewedStub = sinon.stub( |
| changeModel, |
| 'setReviewedFilesStatus' |
| ); |
| |
| element.patchNum = EDIT; |
| element.basePatchNum = 1 as BasePatchSetNum; |
| await waitEventLoop(); |
| |
| 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, 'initCursor'); |
| |
| element.focusLineNum = 123; |
| |
| 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, |
| }; |
| browserModel.setScreenWidth(0); |
| |
| const userStub = sinon.stub(userModel, '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')); |
| }); |
| |
| test('initCursor', () => { |
| assertIsDefined(element.cursor); |
| assert.isNotOk(element.cursor.initialLineNumber); |
| |
| // Does nothing when viewState specify no cursor address: |
| element.leftSide = false; |
| element.initCursor(); |
| assert.isNotOk(element.cursor.initialLineNumber); |
| |
| // Does nothing when viewState specify side but no number: |
| element.leftSide = true; |
| element.initCursor(); |
| assert.isNotOk(element.cursor.initialLineNumber); |
| |
| // Revision hash: specifies lineNum but not side. |
| |
| element.focusLineNum = 234; |
| element.leftSide = false; |
| element.initCursor(); |
| assert.equal(element.cursor.initialLineNumber, 234); |
| assert.equal(element.cursor.side, Side.RIGHT); |
| |
| // Base hash: specifies lineNum and side. |
| element.focusLineNum = 345; |
| element.leftSide = true; |
| element.initCursor(); |
| assert.equal(element.cursor.initialLineNumber, 345); |
| assert.equal(element.cursor.side, Side.LEFT); |
| |
| // Specifies right side: |
| element.focusLineNum = 123; |
| element.leftSide = false; |
| element.initCursor(); |
| assert.equal(element.cursor.initialLineNumber, 123); |
| assert.equal(element.cursor.side, Side.RIGHT); |
| }); |
| |
| test('getLineOfInterest', () => { |
| element.leftSide = false; |
| assert.isUndefined(element.getLineOfInterest()); |
| |
| element.focusLineNum = 12; |
| element.leftSide = false; |
| let result = element.getLineOfInterest(); |
| assert.isOk(result); |
| assert.equal(result!.lineNum, 12); |
| assert.equal(result!.side, Side.RIGHT); |
| |
| element.leftSide = true; |
| result = element.getLineOfInterest(); |
| 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); |
| |
| element.changeNum = 321 as NumericChangeId; |
| element.change = { |
| ...createParsedChange(), |
| _number: 321 as NumericChangeId, |
| project: 'foo/bar' as RepoName, |
| }; |
| element.patchNum = 5 as RevisionPatchSetNum; |
| element.basePatchNum = 3 as BasePatchSetNum; |
| 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); |
| |
| element.changeNum = 321 as NumericChangeId; |
| element.change = { |
| ...createParsedChange(), |
| _number: 321 as NumericChangeId, |
| project: 'foo/bar' as RepoName, |
| }; |
| element.patchNum = 5 as RevisionPatchSetNum; |
| element.basePatchNum = 3 as BasePatchSetNum; |
| const e = {detail: {number: 123, side: Side.LEFT}} as CustomEvent; |
| |
| element.onLineSelected(e); |
| |
| assert.isTrue(replaceStateStub.called); |
| }); |
| |
| test('handleToggleDiffMode', () => { |
| const userStub = sinon.stub(userModel, '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('findFileWithComment', () => { |
| test('empty file list', () => { |
| element.changeComments = new ChangeComments({ |
| 'path/one.jpg': [createComment('c1', 1, 1, 'path/one.jpg')], |
| 'path/three.wav': [createComment('c1', 1, 1, 'path/three.wav')], |
| }); |
| element.path = 'path/two.m4v'; |
| assert.isUndefined(element.findFileWithComment(-1)); |
| assert.isUndefined(element.findFileWithComment(1)); |
| }); |
| |
| test('finds skips', () => { |
| const fileList = ['path/one.jpg', 'path/two.m4v', 'path/three.wav']; |
| element.files = {sortedPaths: fileList, changeFilesByPath: {}}; |
| element.path = fileList[1]; |
| element.changeComments = new ChangeComments({ |
| 'path/one.jpg': [createComment('c1', 1, 1, 'path/one.jpg')], |
| 'path/three.wav': [createComment('c1', 1, 1, 'path/three.wav')], |
| }); |
| |
| assert.equal(element.findFileWithComment(-1), fileList[0]); |
| assert.equal(element.findFileWithComment(1), fileList[2]); |
| |
| element.changeComments = new ChangeComments({ |
| 'path/one.jpg': [createComment('c1', 1, 1, 'path/one.jpg')], |
| 'path/two.m4v': [createComment('c1', 1, 1, 'path/two.m4v')], |
| 'path/three.wav': [createComment('c1', 1, 1, 'path/three.wav')], |
| }); |
| |
| assert.equal(element.findFileWithComment(-1), fileList[0]); |
| assert.equal(element.findFileWithComment(1), fileList[2]); |
| |
| element.path = fileList[0]; |
| |
| assert.isUndefined(element.findFileWithComment(-1)); |
| assert.equal(element.findFileWithComment(1), fileList[1]); |
| |
| element.path = fileList[2]; |
| |
| assert.equal(element.findFileWithComment(-1), fileList[1]); |
| assert.isUndefined(element.findFileWithComment(1)); |
| }); |
| |
| suite('skip next/previous', () => { |
| setup(() => { |
| element.files = getFilesFromFileList([ |
| 'path/one.jpg', |
| 'path/two.m4v', |
| 'path/three.wav', |
| ]); |
| element.patchNum = 2 as RevisionPatchSetNum; |
| element.basePatchNum = 1 as BasePatchSetNum; |
| }); |
| |
| suite('moveToFileWithComment previous', () => { |
| test('no previous', async () => { |
| element.changeComments = new ChangeComments({ |
| 'path/three.wav': [createComment('c1', 1, 1, 'path/three.wav')], |
| }); |
| element.path = element.files.sortedPaths[1]; |
| await element.updateComplete; |
| |
| element.moveToFileWithComment(-1); |
| assert.isTrue(navToChangeStub.calledOnce); |
| assert.isFalse(navToDiffStub.called); |
| }); |
| |
| test('w/ previous', async () => { |
| element.changeComments = new ChangeComments({ |
| 'path/one.jpg': [createComment('c1', 1, 1, 'path/one.jpg')], |
| 'path/three.wav': [createComment('c1', 1, 1, 'path/three.wav')], |
| }); |
| element.path = element.files.sortedPaths[1]; |
| await element.updateComplete; |
| |
| element.moveToFileWithComment(-1); |
| assert.isFalse(navToChangeStub.called); |
| assert.isTrue(navToDiffStub.calledOnce); |
| }); |
| }); |
| |
| suite('moveToFileWithComment next', () => { |
| test('no previous', async () => { |
| element.changeComments = new ChangeComments({ |
| 'path/one.jpg': [createComment('c1', 1, 1, 'path/one.jpg')], |
| }); |
| element.path = element.files.sortedPaths[1]; |
| await element.updateComplete; |
| |
| element.moveToFileWithComment(1); |
| assert.isTrue(navToChangeStub.calledOnce); |
| assert.isFalse(navToDiffStub.called); |
| }); |
| |
| test('w/ previous', async () => { |
| element.changeComments = new ChangeComments({ |
| 'path/one.jpg': [createComment('c1', 1, 1, 'path/one.jpg')], |
| 'path/three.wav': [createComment('c1', 1, 1, 'path/three.wav')], |
| }); |
| element.path = element.files.sortedPaths[1]; |
| await element.updateComplete; |
| |
| element.moveToFileWithComment(1); |
| assert.isFalse(navToChangeStub.called); |
| assert.isTrue(navToDiffStub.calledOnce); |
| }); |
| }); |
| }); |
| }); |
| |
| 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.patchNum = 1 as RevisionPatchSetNum; |
| element.basePatchNum = PARENT; |
| await element.updateComplete; |
| |
| let checkbox = queryAndAssert(element, '#reviewed'); |
| assert.isTrue(isVisible(checkbox)); |
| |
| element.patchNum = EDIT; |
| await element.updateComplete; |
| |
| checkbox = queryAndAssert(element, '#reviewed'); |
| assert.isFalse(isVisible(checkbox)); |
| }); |
| }); |
| |
| suite('switching files', () => { |
| let dispatchEventStub: SinonStubbedMember<Element['dispatchEvent']>; |
| let navToFileStub: SinonStubbedMember<GrDiffView['navToFile']>; |
| let moveToPreviousChunkStub: SinonStubbedMember< |
| GrDiffCursor['moveToPreviousChunk'] |
| >; |
| let moveToNextChunkStub: SinonStubbedMember< |
| GrDiffCursor['moveToNextChunk'] |
| >; |
| let isAtStartStub: SinonStubbedMember<GrDiffCursor['isAtStart']>; |
| let isAtEndStub: SinonStubbedMember<GrDiffCursor['isAtEnd']>; |
| 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, '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, '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, 'initCursor'); |
| |
| // Load file1 |
| viewModel.setState({ |
| ...createDiffViewState(), |
| patchNum: 1 as RevisionPatchSetNum, |
| repo: 'test-project' as RepoName, |
| diffView: {path: 'file1'}, |
| }); |
| element.patchNum = 1 as RevisionPatchSetNum; |
| element.basePatchNum = PARENT; |
| element.change = { |
| ...createParsedChange(), |
| revisions: createRevisions(1), |
| }; |
| await element.updateComplete; |
| assert.isFalse(navToDiffStub.called); |
| |
| // Switch to file2 |
| element.handleFileChange( |
| new CustomEvent('value-change', {detail: {value: 'file2'}}) |
| ); |
| assert.isTrue(navToDiffStub.calledOnce); |
| assert.deepEqual(navToDiffStub.lastCall.firstArg, {path: 'file2'}); |
| |
| // This is to mock the param change triggered by above navigate |
| viewModel.setState({ |
| ...createDiffViewState(), |
| patchNum: 1 as RevisionPatchSetNum, |
| repo: 'test-project' as RepoName, |
| diffView: {path: 'file2'}, |
| }); |
| element.patchNum = 1 as RevisionPatchSetNum; |
| element.basePatchNum = PARENT; |
| |
| // No extra call |
| assert.isTrue(navToDiffStub.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.patchNum = 1 as RevisionPatchSetNum; |
| element.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.patchNum = 3 as RevisionPatchSetNum; |
| element.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' |
| ); |
| }); |
| }); |
| }); |