| /** | |
| * @license | |
| * Copyright 2018 Google LLC | |
| * SPDX-License-Identifier: Apache-2.0 | |
| */ | |
| import * as sinon from 'sinon'; | |
| import '../../../test/common-test-setup'; | |
| import './gr-diff-host'; | |
| import { | |
| ChangeStatus, | |
| CommentSide, | |
| createDefaultDiffPrefs, | |
| Side, | |
| } from '../../../constants/constants'; | |
| import { | |
| createAccountDetailWithId, | |
| createBlame, | |
| createChange, | |
| createComment, | |
| createCommentThread, | |
| createDiff, | |
| createEditRevision, | |
| createPatchRange, | |
| createRevision, | |
| createRunResult, | |
| } from '../../../test/test-data-generators'; | |
| import { | |
| addListenerForTest, | |
| mockPromise, | |
| query, | |
| queryAll, | |
| queryAndAssert, | |
| stubReporting, | |
| stubRestApi, | |
| } from '../../../test/test-utils'; | |
| import { | |
| Base64ImageFile, | |
| BasePatchSetNum, | |
| BlameInfo, | |
| BranchName, | |
| CommentRange, | |
| CommentThread, | |
| DraftInfo, | |
| EDIT, | |
| NumericChangeId, | |
| PARENT, | |
| PatchSetNum, | |
| PatchSetNumber, | |
| RevisionInfo, | |
| RevisionPatchSetNum, | |
| } from '../../../types/common'; | |
| import {CoverageType} from '../../../types/types'; | |
| import {GrDiffHost} from './gr-diff-host'; | |
| import { | |
| DiffInfo, | |
| DiffViewMode, | |
| GrDiffLineType, | |
| IgnoreWhitespaceType, | |
| } from '../../../api/diff'; | |
| import { | |
| GrDiffGroup, | |
| GrDiffGroupType, | |
| } from '../../../embed/diff/gr-diff/gr-diff-group'; | |
| import {GrDiffLine} from '../../../embed/diff/gr-diff/gr-diff-line'; | |
| import {Interaction, Timing} from '../../../constants/reporting'; | |
| import {ErrorCallback} from '../../../api/rest'; | |
| import {SinonStub, SinonStubbedMember} from 'sinon'; | |
| import {RunResult} from '../../../models/checks/checks-model'; | |
| import {assertIsDefined} from '../../../utils/common-util'; | |
| import {assert, fixture, html} from '@open-wc/testing'; | |
| import {testResolver} from '../../../test/common-test-setup'; | |
| import {navigationToken} from '../../core/gr-navigation/gr-navigation'; | |
| import {UserModel, userModelToken} from '../../../models/user/user-model'; | |
| import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader'; | |
| import {ReportingService} from '../../../services/gr-reporting/gr-reporting'; | |
| import {RestApiService} from '../../../services/gr-rest-api/gr-rest-api'; | |
| import { | |
| CommentsModel, | |
| commentsModelToken, | |
| } from '../../../models/comments/comments-model'; | |
| import {throwingErrorCallback} from '../../shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper'; | |
| suite('gr-diff-host tests', () => { | |
| let element: GrDiffHost; | |
| let account = createAccountDetailWithId(1); | |
| let getDiffRestApiStub: SinonStubbedMember<RestApiService['getDiff']>; | |
| let userModel: UserModel; | |
| setup(async () => { | |
| stubRestApi('getAccount').callsFake(() => Promise.resolve(account)); | |
| element = await fixture(html`<gr-diff-host | |
| .changeNum=${123 as NumericChangeId} | |
| .path=${'some/path'} | |
| .file=${{path: 'some/path'}} | |
| .change=${createChange()} | |
| .patchRange=${createPatchRange()} | |
| ></gr-diff-host>`); | |
| getDiffRestApiStub = stubRestApi('getDiff'); | |
| // Fall back in case a test forgets to set one up | |
| getDiffRestApiStub.returns(Promise.resolve(createDiff())); | |
| await element.updateComplete; | |
| userModel = testResolver(userModelToken); | |
| }); | |
| suite('render reporting', () => { | |
| test('ends total and syntax timer after syntax layer', async () => { | |
| const displayedStub = stubReporting('diffViewContentDisplayed'); | |
| element.patchRange = createPatchRange(); | |
| element.change = createChange(); | |
| element.prefs = createDefaultDiffPrefs(); | |
| await element.updateComplete; | |
| // Force a reload because it's not possible to wait on the reload called | |
| // from update(). | |
| await element.reload(); | |
| const timeEndStub = sinon.stub(element.reporting, 'timeEnd'); | |
| let notifySyntaxProcessed: () => void = () => {}; | |
| sinon.stub(element.syntaxLayer, 'process').returns( | |
| new Promise(resolve => { | |
| notifySyntaxProcessed = resolve; | |
| }) | |
| ); | |
| const promise = element.reload(true); | |
| // Multiple cascading microtasks are scheduled. | |
| notifySyntaxProcessed(); | |
| await element.updateComplete; | |
| await promise; | |
| const calls = timeEndStub.getCalls(); | |
| assert.equal(calls.length, 4); | |
| assert.equal(calls[0].args[0], 'Diff Load Render'); | |
| assert.equal(calls[1].args[0], 'Diff Content Render'); | |
| assert.equal(calls[2].args[0], 'Diff Syntax Render'); | |
| assert.equal(calls[3].args[0], 'Diff Total Render'); | |
| assert.isTrue(displayedStub.called); | |
| }); | |
| test('completes reload promise after syntax layer processing', async () => { | |
| let notifySyntaxProcessed: () => void = () => {}; | |
| sinon.stub(element.syntaxLayer, 'process').returns( | |
| new Promise(resolve => { | |
| notifySyntaxProcessed = resolve; | |
| }) | |
| ); | |
| getDiffRestApiStub.returns(Promise.resolve(createDiff())); | |
| element.patchRange = createPatchRange(); | |
| element.change = createChange(); | |
| let reloadComplete = false; | |
| element.prefs = createDefaultDiffPrefs(); | |
| const promise = mockPromise(); | |
| element.reload().then(() => { | |
| reloadComplete = true; | |
| promise.resolve(); | |
| }); | |
| // Multiple cascading microtasks are scheduled. | |
| assert.isFalse(reloadComplete); | |
| notifySyntaxProcessed(); | |
| await promise; | |
| assert.isTrue(reloadComplete); | |
| }); | |
| }); | |
| test('render', () => { | |
| assert.shadowDom.equal( | |
| element, | |
| /* HTML */ ` | |
| <gr-diff | |
| id="diff" | |
| style="--line-limit-marker:-1px; --content-width:100ch; --diff-max-width:none; --font-size:12px;" | |
| > | |
| </gr-diff> | |
| `, | |
| {ignoreAttributes: ['style']} | |
| ); | |
| }); | |
| test('prefetch getDiff', async () => { | |
| getDiffRestApiStub.returns(Promise.resolve(createDiff())); | |
| element.changeNum = 123 as NumericChangeId; | |
| element.patchRange = createPatchRange(); | |
| element.path = 'file.txt'; | |
| element.prefetchDiff(); | |
| await element.getDiff(); | |
| assert.isTrue(getDiffRestApiStub.calledOnce); | |
| }); | |
| test('getDiff handles undefined diff responses', async () => { | |
| getDiffRestApiStub.returns(Promise.resolve(undefined)); | |
| element.changeNum = 123 as NumericChangeId; | |
| element.patchRange = createPatchRange(); | |
| element.path = 'file.txt'; | |
| await element.getDiff(); | |
| }); | |
| test('reload resolves on error', () => { | |
| const onErrStub = sinon.stub(element, 'handleGetDiffError'); | |
| const error = new Response(null, {status: 500}); | |
| getDiffRestApiStub.callsFake( | |
| ( | |
| _1: NumericChangeId, | |
| _2: PatchSetNum, | |
| _3: PatchSetNum, | |
| _4: string, | |
| _5?: IgnoreWhitespaceType, | |
| onErr?: ErrorCallback | |
| ) => { | |
| if (onErr) onErr(error); | |
| return Promise.resolve(undefined); | |
| } | |
| ); | |
| element.patchRange = createPatchRange(); | |
| return element.reload().then(() => { | |
| assert.isTrue(onErrStub.calledOnce); | |
| }); | |
| }); | |
| suite('handleGetDiffError', () => { | |
| let serverErrorStub: sinon.SinonStub; | |
| let pageErrorStub: sinon.SinonStub; | |
| setup(() => { | |
| serverErrorStub = sinon.stub(); | |
| addListenerForTest(document, 'server-error', serverErrorStub); | |
| pageErrorStub = sinon.stub(); | |
| addListenerForTest(document, 'page-error', pageErrorStub); | |
| }); | |
| test('page error on HTTP-409', () => { | |
| element.handleGetDiffError({status: 409} as Response); | |
| assert.isTrue(serverErrorStub.calledOnce); | |
| assert.isFalse(pageErrorStub.called); | |
| assert.isNotOk(element.errorMessage); | |
| }); | |
| test('server error on non-HTTP-409', () => { | |
| element.handleGetDiffError({ | |
| status: 500, | |
| text: () => Promise.resolve(''), | |
| } as Response); | |
| assert.isFalse(serverErrorStub.called); | |
| assert.isTrue(pageErrorStub.calledOnce); | |
| assert.isNotOk(element.errorMessage); | |
| }); | |
| test('error message if showLoadFailure', () => { | |
| element.showLoadFailure = true; | |
| element.handleGetDiffError({ | |
| status: 500, | |
| statusText: 'Failure!', | |
| } as Response); | |
| assert.isFalse(serverErrorStub.called); | |
| assert.isFalse(pageErrorStub.called); | |
| assert.equal( | |
| element.errorMessage, | |
| 'Encountered error when loading the diff: 500 Failure!' | |
| ); | |
| }); | |
| }); | |
| suite('image diffs', () => { | |
| let mockFile1: Base64ImageFile; | |
| let mockFile2: Base64ImageFile; | |
| setup(() => { | |
| mockFile1 = { | |
| body: | |
| 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' + | |
| 'wsAAAAAAAAAAAAAAAAA/w==', | |
| type: 'image/bmp', | |
| _expectedType: 'image/bmp', | |
| _name: 'carrot.bmp', | |
| }; | |
| mockFile2 = { | |
| body: | |
| 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' + | |
| 'wsAAAAAAAAAAAAA/////w==', | |
| type: 'image/bmp', | |
| _expectedType: 'image/bmp', | |
| _name: 'potato.bmp', | |
| }; | |
| element.patchRange = createPatchRange(); | |
| element.change = createChange(); | |
| }); | |
| test('renders image diffs with same file name', async () => { | |
| const mockDiff: DiffInfo = { | |
| meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66}, | |
| meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 560}, | |
| intraline_status: 'OK', | |
| change_type: 'MODIFIED', | |
| diff_header: [ | |
| 'diff --git a/carrot.jpg b/carrot.jpg', | |
| 'index 2adc47d..f9c2f2c 100644', | |
| '--- a/carrot.jpg', | |
| '+++ b/carrot.jpg', | |
| 'Binary files differ', | |
| ], | |
| content: [{skip: 66}], | |
| binary: true, | |
| }; | |
| getDiffRestApiStub.returns(Promise.resolve(mockDiff)); | |
| stubRestApi('getImagesForDiff').returns( | |
| Promise.resolve({ | |
| baseImage: { | |
| ...mockFile1, | |
| _expectedType: 'image/jpeg', | |
| _name: 'carrot.jpg', | |
| }, | |
| revisionImage: { | |
| ...mockFile2, | |
| _expectedType: 'image/jpeg', | |
| _name: 'carrot.jpg', | |
| }, | |
| }) | |
| ); | |
| element.prefs = createDefaultDiffPrefs(); | |
| element.reload(); | |
| await element.waitForReloadToRender(); | |
| // Left image rendered with the parent commit's version of the file. | |
| assertIsDefined(element.diffElement); | |
| const leftImage = queryAndAssert(element.diffElement, 'td.left img'); | |
| const leftLabel = queryAndAssert(element.diffElement, 'td.left label'); | |
| const leftLabelContent = leftLabel.querySelector('.label'); | |
| const leftLabelName = leftLabel.querySelector('.name'); | |
| const rightImage = queryAndAssert(element.diffElement, 'td.right img'); | |
| const rightLabel = queryAndAssert(element.diffElement, 'td.right label'); | |
| const rightLabelContent = rightLabel.querySelector('.label'); | |
| const rightLabelName = rightLabel.querySelector('.name'); | |
| assert.isOk(leftImage); | |
| assert.equal( | |
| leftImage.getAttribute('src'), | |
| 'data:image/bmp;base64,' + mockFile1.body | |
| ); | |
| assert.isTrue(leftLabelContent?.textContent?.includes('image/bmp')); | |
| assert.isNotOk(leftLabelName); | |
| assert.isOk(rightImage); | |
| assert.equal( | |
| rightImage.getAttribute('src'), | |
| 'data:image/bmp;base64,' + mockFile2.body | |
| ); | |
| assert.isTrue(rightLabelContent?.textContent?.includes('image/bmp')); | |
| assert.isNotOk(rightLabelName); | |
| }); | |
| test('renders image diffs with a different file name', async () => { | |
| const mockDiff: DiffInfo = { | |
| meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66}, | |
| meta_b: {name: 'carrot2.jpg', content_type: 'image/jpeg', lines: 560}, | |
| intraline_status: 'OK', | |
| change_type: 'MODIFIED', | |
| diff_header: [ | |
| 'diff --git a/carrot.jpg b/carrot2.jpg', | |
| 'index 2adc47d..f9c2f2c 100644', | |
| '--- a/carrot.jpg', | |
| '+++ b/carrot2.jpg', | |
| 'Binary files differ', | |
| ], | |
| content: [{skip: 66}], | |
| binary: true, | |
| }; | |
| getDiffRestApiStub.returns(Promise.resolve(mockDiff)); | |
| stubRestApi('getImagesForDiff').returns( | |
| Promise.resolve({ | |
| baseImage: { | |
| ...mockFile1, | |
| _expectedType: 'image/jpeg', | |
| _name: 'carrot.jpg', | |
| }, | |
| revisionImage: { | |
| ...mockFile2, | |
| _expectedType: 'image/jpeg', | |
| _name: 'carrot2.jpg', | |
| }, | |
| }) | |
| ); | |
| element.prefs = createDefaultDiffPrefs(); | |
| element.reload(); | |
| await element.waitForReloadToRender(); | |
| assertIsDefined(element.diffElement); | |
| // Left image rendered with the parent commit's version of the file. | |
| const leftImage = queryAndAssert(element.diffElement, 'td.left img'); | |
| const leftLabel = queryAndAssert(element.diffElement, 'td.left label'); | |
| const leftLabelContent = leftLabel.querySelector('.label'); | |
| const leftLabelName = leftLabel.querySelector('.name'); | |
| const rightImage = queryAndAssert(element.diffElement, 'td.right img'); | |
| const rightLabel = queryAndAssert(element.diffElement, 'td.right label'); | |
| const rightLabelContent = rightLabel.querySelector('.label'); | |
| const rightLabelName = rightLabel.querySelector('.name'); | |
| assert.isOk(rightLabelName); | |
| assert.isOk(leftLabelName); | |
| assert.equal(leftLabelName?.textContent, mockDiff.meta_a?.name); | |
| assert.equal(rightLabelName?.textContent, mockDiff.meta_b?.name); | |
| assert.isOk(leftImage); | |
| assert.equal( | |
| leftImage.getAttribute('src'), | |
| 'data:image/bmp;base64,' + mockFile1.body | |
| ); | |
| assert.isTrue(leftLabelContent?.textContent?.includes('image/bmp')); | |
| assert.isOk(rightImage); | |
| assert.equal( | |
| rightImage.getAttribute('src'), | |
| 'data:image/bmp;base64,' + mockFile2.body | |
| ); | |
| assert.isTrue(rightLabelContent?.textContent?.includes('image/bmp')); | |
| }); | |
| test('renders added image', async () => { | |
| const mockDiff: DiffInfo = { | |
| meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 560}, | |
| intraline_status: 'OK', | |
| change_type: 'ADDED', | |
| diff_header: [ | |
| 'diff --git a/carrot.jpg b/carrot.jpg', | |
| 'index 0000000..f9c2f2c 100644', | |
| '--- /dev/null', | |
| '+++ b/carrot.jpg', | |
| 'Binary files differ', | |
| ], | |
| content: [{skip: 66}], | |
| binary: true, | |
| }; | |
| getDiffRestApiStub.returns(Promise.resolve(mockDiff)); | |
| stubRestApi('getImagesForDiff').returns( | |
| Promise.resolve({ | |
| baseImage: null, | |
| revisionImage: { | |
| ...mockFile2, | |
| _expectedType: 'image/jpeg', | |
| _name: 'carrot2.jpg', | |
| }, | |
| }) | |
| ); | |
| element.prefs = createDefaultDiffPrefs(); | |
| element.reload(); | |
| await element.waitForReloadToRender().then(() => { | |
| assertIsDefined(element.diffElement); | |
| const leftImage = query(element.diffElement, 'td.left img'); | |
| const rightImage = queryAndAssert(element.diffElement, 'td.right img'); | |
| assert.isNotOk(leftImage); | |
| assert.isOk(rightImage); | |
| }); | |
| }); | |
| test('renders removed image', async () => { | |
| const mockDiff: DiffInfo = { | |
| meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 560}, | |
| intraline_status: 'OK', | |
| change_type: 'DELETED', | |
| diff_header: [ | |
| 'diff --git a/carrot.jpg b/carrot.jpg', | |
| 'index f9c2f2c..0000000 100644', | |
| '--- a/carrot.jpg', | |
| '+++ /dev/null', | |
| 'Binary files differ', | |
| ], | |
| content: [{skip: 66}], | |
| binary: true, | |
| }; | |
| getDiffRestApiStub.returns(Promise.resolve(mockDiff)); | |
| stubRestApi('getImagesForDiff').returns( | |
| Promise.resolve({ | |
| baseImage: { | |
| ...mockFile1, | |
| _expectedType: 'image/jpeg', | |
| _name: 'carrot.jpg', | |
| }, | |
| revisionImage: null, | |
| }) | |
| ); | |
| element.prefs = createDefaultDiffPrefs(); | |
| element.reload(); | |
| await element.waitForReloadToRender().then(() => { | |
| assertIsDefined(element.diffElement); | |
| const leftImage = queryAndAssert(element.diffElement, 'td.left img'); | |
| const rightImage = query(element.diffElement, 'td.right img'); | |
| assert.isOk(leftImage); | |
| assert.isNotOk(rightImage); | |
| }); | |
| }); | |
| test('does not render disallowed image type', async () => { | |
| const mockDiff: DiffInfo = { | |
| meta_a: { | |
| name: 'carrot.jpg', | |
| content_type: 'image/jpeg-evil', | |
| lines: 560, | |
| }, | |
| intraline_status: 'OK', | |
| change_type: 'DELETED', | |
| diff_header: [ | |
| 'diff --git a/carrot.jpg b/carrot.jpg', | |
| 'index f9c2f2c..0000000 100644', | |
| '--- a/carrot.jpg', | |
| '+++ /dev/null', | |
| 'Binary files differ', | |
| ], | |
| content: [{skip: 66}], | |
| binary: true, | |
| }; | |
| mockFile1.type = 'image/jpeg-evil'; | |
| getDiffRestApiStub.returns(Promise.resolve(mockDiff)); | |
| stubRestApi('getImagesForDiff').returns( | |
| Promise.resolve({ | |
| baseImage: { | |
| ...mockFile1, | |
| _expectedType: 'image/jpeg', | |
| _name: 'carrot.jpg', | |
| }, | |
| revisionImage: null, | |
| }) | |
| ); | |
| element.prefs = createDefaultDiffPrefs(); | |
| element.updateComplete.then(() => { | |
| assertIsDefined(element.diffElement); | |
| const leftImage = query(element.diffElement, 'td.left img'); | |
| assert.isNotOk(leftImage); | |
| }); | |
| }); | |
| }); | |
| test('cannot create comments when not logged in', () => { | |
| userModel.setAccount(undefined); | |
| element.patchRange = createPatchRange(); | |
| const showAuthRequireSpy = sinon.spy(); | |
| element.addEventListener('show-auth-required', showAuthRequireSpy); | |
| element.dispatchEvent( | |
| new CustomEvent('create-comment', { | |
| detail: { | |
| lineNum: 3, | |
| side: Side.LEFT, | |
| path: '/p', | |
| }, | |
| }) | |
| ); | |
| assertIsDefined(element.diffElement); | |
| const threads = queryAll(element.diffElement, 'gr-comment-thread'); | |
| assert.equal(threads.length, 0); | |
| assert.isTrue(showAuthRequireSpy.called); | |
| }); | |
| test('delegates getCursorStops()', () => { | |
| const returnValue = [document.createElement('b')]; | |
| assertIsDefined(element.diffElement); | |
| const stub = sinon | |
| .stub(element.diffElement, 'getCursorStops') | |
| .returns(returnValue); | |
| assert.equal(element.getCursorStops(), returnValue); | |
| assert.isTrue(stub.calledOnce); | |
| assert.equal(stub.lastCall.args.length, 0); | |
| }); | |
| test('delegates isRangeSelected()', () => { | |
| const returnValue = true; | |
| assertIsDefined(element.diffElement); | |
| const stub = sinon | |
| .stub(element.diffElement, 'isRangeSelected') | |
| .returns(returnValue); | |
| assert.equal(element.isRangeSelected(), returnValue); | |
| assert.isTrue(stub.calledOnce); | |
| assert.equal(stub.lastCall.args.length, 0); | |
| }); | |
| test('clearBlame', async () => { | |
| element.blame = []; | |
| await element.updateComplete; | |
| assertIsDefined(element.diffElement); | |
| const isBlameLoadedStub = sinon.stub(); | |
| element.addEventListener('is-blame-loaded-changed', isBlameLoadedStub); | |
| element.clearBlame(); | |
| await element.updateComplete; | |
| assert.isNull(element.blame); | |
| assert.isTrue(isBlameLoadedStub.calledOnce); | |
| assert.isFalse(isBlameLoadedStub.args[0][0].detail.value); | |
| }); | |
| test('loadBlame', async () => { | |
| const mockBlame: BlameInfo[] = [createBlame()]; | |
| const showAlertStub = sinon.stub(); | |
| element.addEventListener('show-alert', showAlertStub); | |
| const getBlameStub = stubRestApi('getBlame').returns( | |
| Promise.resolve(mockBlame) | |
| ); | |
| const changeNum = 42 as NumericChangeId; | |
| element.changeNum = changeNum; | |
| element.patchRange = createPatchRange(); | |
| element.path = 'foo/bar.baz'; | |
| await element.updateComplete; | |
| const isBlameLoadedStub = sinon.stub(); | |
| element.addEventListener('is-blame-loaded-changed', isBlameLoadedStub); | |
| return element.loadBlame().then(() => { | |
| assert.isTrue( | |
| getBlameStub.calledWithExactly( | |
| changeNum, | |
| 1 as RevisionPatchSetNum, | |
| 'foo/bar.baz', | |
| true | |
| ) | |
| ); | |
| assert.isFalse(showAlertStub.called); | |
| assert.equal(element.blame, mockBlame); | |
| assert.isTrue(isBlameLoadedStub.calledOnce); | |
| assert.isTrue(isBlameLoadedStub.args[0][0].detail.value); | |
| }); | |
| }); | |
| test('loadBlame empty', async () => { | |
| const mockBlame: BlameInfo[] = []; | |
| const showAlertStub = sinon.stub(); | |
| const isBlameLoadedStub = sinon.stub(); | |
| element.addEventListener('show-alert', showAlertStub); | |
| element.addEventListener('is-blame-loaded-changed', isBlameLoadedStub); | |
| stubRestApi('getBlame').returns(Promise.resolve(mockBlame)); | |
| const changeNum = 42 as NumericChangeId; | |
| element.changeNum = changeNum; | |
| element.patchRange = createPatchRange(); | |
| element.path = 'foo/bar.baz'; | |
| await element.updateComplete; | |
| return element | |
| .loadBlame() | |
| .then(() => { | |
| assert.isTrue(false, 'Promise should not resolve'); | |
| }) | |
| .catch(() => { | |
| assert.isTrue(showAlertStub.calledOnce); | |
| assert.isNull(element.blame); | |
| // We don't expect a call because | |
| assert.isTrue(isBlameLoadedStub.notCalled); | |
| }); | |
| }); | |
| test('delegates toggleAllContext()', () => { | |
| assertIsDefined(element.diffElement); | |
| const stub = sinon.stub(element.diffElement, 'toggleAllContext'); | |
| element.toggleAllContext(); | |
| assert.isTrue(stub.calledOnce); | |
| assert.equal(stub.lastCall.args.length, 0); | |
| }); | |
| test('passes in noAutoRender', async () => { | |
| const value = true; | |
| element.noAutoRender = value; | |
| await element.updateComplete; | |
| assertIsDefined(element.diffElement); | |
| assert.equal(element.diffElement.noAutoRender, value); | |
| }); | |
| test('passes in path', async () => { | |
| const value = 'some/file/path'; | |
| element.path = value; | |
| await element.updateComplete; | |
| assertIsDefined(element.diffElement); | |
| assert.equal(element.diffElement.path, value); | |
| }); | |
| test('passes in prefs', async () => { | |
| const value = createDefaultDiffPrefs(); | |
| element.prefs = value; | |
| await element.updateComplete; | |
| assertIsDefined(element.diffElement); | |
| assert.equal(element.diffElement.prefs, value); | |
| }); | |
| test('passes in hidden', async () => { | |
| const value = true; | |
| element.hidden = value; | |
| await element.updateComplete; | |
| assertIsDefined(element.diffElement); | |
| assert.equal(element.diffElement.hidden, value); | |
| assert.isNotNull(element.getAttribute('hidden')); | |
| }); | |
| test('passes in noRenderOnPrefsChange', async () => { | |
| const value = true; | |
| element.noRenderOnPrefsChange = value; | |
| await element.updateComplete; | |
| assertIsDefined(element.diffElement); | |
| assert.equal(element.diffElement.noRenderOnPrefsChange, value); | |
| }); | |
| test('passes in lineWrapping', async () => { | |
| const value = true; | |
| element.lineWrapping = value; | |
| await element.updateComplete; | |
| assertIsDefined(element.diffElement); | |
| assert.equal(element.diffElement.lineWrapping, value); | |
| }); | |
| test('passes in viewMode', async () => { | |
| const value = DiffViewMode.SIDE_BY_SIDE; | |
| element.viewMode = value; | |
| await element.updateComplete; | |
| assertIsDefined(element.diffElement); | |
| assert.equal(element.diffElement.viewMode, value); | |
| }); | |
| test('passes in lineOfInterest', async () => { | |
| const value = {lineNum: 123, side: Side.LEFT}; | |
| element.lineOfInterest = value; | |
| await element.updateComplete; | |
| assertIsDefined(element.diffElement); | |
| assert.equal(element.diffElement.lineOfInterest, value); | |
| }); | |
| suite('reportDiff', () => { | |
| let reportStub: SinonStubbedMember<ReportingService['reportInteraction']>; | |
| setup(async () => { | |
| element.patchRange = createPatchRange(1, 2); | |
| await element.updateComplete; | |
| reportStub = sinon.stub(element.reporting, 'reportInteraction'); | |
| reportStub.reset(); | |
| }); | |
| test('undefined', () => { | |
| element.reportDiff(undefined); | |
| assert.isFalse(reportStub.called); | |
| }); | |
| test('diff w/ no delta', () => { | |
| const diff: DiffInfo = { | |
| ...createDiff(), | |
| content: [{ab: ['foo', 'bar']}, {ab: ['baz', 'foo']}], | |
| }; | |
| element.reportDiff(diff); | |
| assert.isTrue(reportStub.calledOnce); | |
| assert.equal(reportStub.lastCall.args[0], 'rebase-percent-zero'); | |
| assert.isUndefined(reportStub.lastCall.args[1]); | |
| }); | |
| test('diff w/ no rebase delta', () => { | |
| const diff: DiffInfo = { | |
| ...createDiff(), | |
| content: [ | |
| {ab: ['foo', 'bar']}, | |
| {a: ['baz', 'foo']}, | |
| {ab: ['foo', 'bar']}, | |
| {a: ['baz', 'foo'], b: ['bar', 'baz']}, | |
| {ab: ['foo', 'bar']}, | |
| {b: ['baz', 'foo']}, | |
| {ab: ['foo', 'bar']}, | |
| ], | |
| }; | |
| element.reportDiff(diff); | |
| assert.isTrue(reportStub.calledOnce); | |
| assert.equal(reportStub.lastCall.args[0], 'rebase-percent-zero'); | |
| assert.isUndefined(reportStub.lastCall.args[1]); | |
| }); | |
| test('diff w/ some rebase delta', () => { | |
| const diff: DiffInfo = { | |
| ...createDiff(), | |
| content: [ | |
| {ab: ['foo', 'bar']}, | |
| {a: ['baz', 'foo'], due_to_rebase: true}, | |
| {ab: ['foo', 'bar']}, | |
| {a: ['baz', 'foo'], b: ['bar', 'baz']}, | |
| {ab: ['foo', 'bar']}, | |
| {b: ['baz', 'foo'], due_to_rebase: true}, | |
| {ab: ['foo', 'bar']}, | |
| {a: ['baz', 'foo']}, | |
| ], | |
| }; | |
| element.reportDiff(diff); | |
| assert.isTrue(reportStub.calledOnce); | |
| assert.isTrue( | |
| reportStub.calledWith('rebase-percent-nonzero', { | |
| percentRebaseDelta: 50, | |
| }) | |
| ); | |
| }); | |
| test('diff w/ all rebase delta', () => { | |
| const diff: DiffInfo = { | |
| ...createDiff(), | |
| content: [ | |
| { | |
| a: ['foo', 'bar'], | |
| b: ['baz', 'foo'], | |
| due_to_rebase: true, | |
| }, | |
| ], | |
| }; | |
| element.reportDiff(diff); | |
| assert.isTrue(reportStub.calledOnce); | |
| assert.isTrue( | |
| reportStub.calledWith('rebase-percent-nonzero', { | |
| percentRebaseDelta: 100, | |
| }) | |
| ); | |
| }); | |
| test('diff against parent event', () => { | |
| element.patchRange = createPatchRange(); | |
| const diff: DiffInfo = { | |
| ...createDiff(), | |
| content: [ | |
| { | |
| a: ['foo', 'bar'], | |
| b: ['baz', 'foo'], | |
| }, | |
| ], | |
| }; | |
| element.reportDiff(diff); | |
| assert.isTrue(reportStub.calledOnce); | |
| assert.equal(reportStub.lastCall.args[0], 'diff-against-parent'); | |
| assert.isUndefined(reportStub.lastCall.args[1]); | |
| }); | |
| }); | |
| suite('render thread elements', () => { | |
| test('right start_line:1', async () => { | |
| const thread: CommentThread = { | |
| ...createCommentThread([createComment()]), | |
| }; | |
| element.threads = [thread]; | |
| await element.updateComplete; | |
| assert.lightDom.equal( | |
| element.diffElement, | |
| /* HTML */ ` | |
| <gr-comment-thread | |
| class="comment-thread" | |
| diff-side="right" | |
| line-num="1" | |
| slot="right-1" | |
| > | |
| </gr-comment-thread> | |
| ` | |
| ); | |
| }); | |
| test('left start_line:2', async () => { | |
| const thread: CommentThread = { | |
| ...createCommentThread([ | |
| createComment({side: CommentSide.PARENT, line: 2}), | |
| ]), | |
| }; | |
| element.threads = [thread]; | |
| await element.updateComplete; | |
| assert.lightDom.equal( | |
| element.diffElement, | |
| /* HTML */ ` | |
| <gr-comment-thread | |
| class="comment-thread" | |
| diff-side="left" | |
| line-num="2" | |
| slot="left-2" | |
| > | |
| </gr-comment-thread> | |
| ` | |
| ); | |
| }); | |
| test('threads are cleared when disabledThreads is true and restored when false', async () => { | |
| const thread: CommentThread = { | |
| ...createCommentThread([createComment()]), | |
| }; | |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| sinon.stub(element as any, 'computeFileThreads').returns([thread]); | |
| element.disabledThreads = false; | |
| element.threads = [thread]; | |
| await element.updateComplete; | |
| assert.equal(element.threads.length, 1); | |
| element.disabledThreads = true; | |
| await element.updateComplete; | |
| assert.equal(element.threads.length, 0); | |
| element.disabledThreads = false; | |
| await element.updateComplete; | |
| assert.equal(element.threads.length, 1); | |
| }); | |
| test('autoSaveDrafts calls autoSave on all comment threads', async () => { | |
| const thread: CommentThread = { | |
| ...createCommentThread([createComment()]), | |
| }; | |
| element.threads = [thread]; | |
| await element.updateComplete; | |
| const threadEl = element.shadowRoot!.querySelector('gr-comment-thread'); | |
| assert.isNotNull(threadEl); | |
| const autoSaveStub = sinon.stub(threadEl, 'autoSave').resolves(); | |
| await element.autoSaveDrafts(); | |
| assert.isTrue(autoSaveStub.calledOnce); | |
| }); | |
| }); | |
| suite('render check elements', () => { | |
| test('start_line:12', async () => { | |
| const result: RunResult = { | |
| ...createRunResult(), | |
| codePointers: [{path: 'a', range: {start_line: 12} as CommentRange}], | |
| }; | |
| element.checks = [result]; | |
| await element.updateComplete; | |
| assert.lightDom.equal( | |
| element.diffElement, | |
| /* HTML */ ` | |
| <gr-diff-check-result | |
| class="comment-thread" | |
| diff-side="right" | |
| line-num="12" | |
| slot="right-12" | |
| > | |
| </gr-diff-check-result> | |
| ` | |
| ); | |
| }); | |
| test('start_line:13 end_line:14 without char positions', async () => { | |
| const result: RunResult = { | |
| ...createRunResult(), | |
| codePointers: [ | |
| {path: 'a', range: {start_line: 13, end_line: 14} as CommentRange}, | |
| ], | |
| }; | |
| element.checks = [result]; | |
| await element.updateComplete; | |
| assert.lightDom.equal( | |
| element.diffElement, | |
| /* HTML */ ` | |
| <gr-diff-check-result | |
| class="comment-thread" | |
| diff-side="right" | |
| line-num="14" | |
| slot="right-14" | |
| > | |
| </gr-diff-check-result> | |
| ` | |
| ); | |
| }); | |
| test('start_line:13 end_line:14 with char positions', async () => { | |
| const result: RunResult = { | |
| ...createRunResult(), | |
| codePointers: [ | |
| { | |
| path: 'a', | |
| range: { | |
| start_line: 13, | |
| end_line: 14, | |
| start_character: 5, | |
| end_character: 7, | |
| }, | |
| }, | |
| ], | |
| }; | |
| element.checks = [result]; | |
| await element.updateComplete; | |
| assert.lightDom.equal( | |
| element.diffElement, | |
| /* HTML */ ` | |
| <gr-diff-check-result | |
| class="comment-thread" | |
| diff-side="right" | |
| line-num="14" | |
| slot="right-14" | |
| range='{"start_line":13,"end_line":14,"start_character":5,"end_character":7}' | |
| > | |
| </gr-diff-check-result> | |
| ` | |
| ); | |
| }); | |
| test('empty range', async () => { | |
| const result: RunResult = { | |
| ...createRunResult(), | |
| codePointers: [{path: 'a', range: {} as CommentRange}], | |
| }; | |
| element.checks = [result]; | |
| await element.updateComplete; | |
| assert.lightDom.equal( | |
| element.diffElement, | |
| /* HTML */ ` | |
| <gr-diff-check-result | |
| class="comment-thread" | |
| diff-side="right" | |
| line-num="FILE" | |
| slot="right-FILE" | |
| > | |
| </gr-diff-check-result> | |
| ` | |
| ); | |
| }); | |
| }); | |
| suite('create-comment', () => { | |
| let addDraftSpy: sinon.SinonSpy; | |
| setup(async () => { | |
| const commentsModel: CommentsModel = testResolver(commentsModelToken); | |
| addDraftSpy = sinon.spy(commentsModel, 'addNewDraft'); | |
| account = createAccountDetailWithId(1); | |
| element.disconnectedCallback(); | |
| element.connectedCallback(); | |
| await element.updateComplete; | |
| }); | |
| test('creates comments if they do not exist yet', async () => { | |
| element.patchRange = createPatchRange(); | |
| element.dispatchEvent( | |
| new CustomEvent('create-comment', { | |
| detail: { | |
| lineNum: 3, | |
| side: Side.LEFT, | |
| path: '/p', | |
| }, | |
| }) | |
| ); | |
| assert.equal(addDraftSpy.callCount, 1); | |
| const draft1: DraftInfo = addDraftSpy.lastCall.firstArg; | |
| assert.equal(draft1.side, CommentSide.PARENT); | |
| assert.equal(draft1.range, undefined); | |
| assert.equal(draft1.patch_set, 1 as RevisionPatchSetNum); | |
| // Try to fetch a thread with a different range. | |
| const range = { | |
| start_line: 1, | |
| start_character: 1, | |
| end_line: 1, | |
| end_character: 3, | |
| }; | |
| element.patchRange = createPatchRange(); | |
| element.dispatchEvent( | |
| new CustomEvent('create-comment', { | |
| detail: { | |
| lineNum: 1, | |
| side: Side.LEFT, | |
| path: '/p', | |
| range, | |
| }, | |
| }) | |
| ); | |
| assert.equal(addDraftSpy.callCount, 2); | |
| const draft2: DraftInfo = addDraftSpy.lastCall.firstArg; | |
| assert.equal(draft2.side, CommentSide.PARENT); | |
| assert.equal(draft2.range, range); | |
| assert.equal(draft2.patch_set, 1 as RevisionPatchSetNum); | |
| }); | |
| test('should not be on parent if on the right', async () => { | |
| element.patchRange = createPatchRange(2, 3); | |
| // Need to recompute threads. | |
| await element.updateComplete; | |
| element.dispatchEvent( | |
| new CustomEvent('create-comment', { | |
| detail: { | |
| side: Side.RIGHT, | |
| }, | |
| }) | |
| ); | |
| assert.equal(addDraftSpy.callCount, 1); | |
| const draft1: DraftInfo = addDraftSpy.lastCall.firstArg; | |
| assert.equal(draft1.side, CommentSide.REVISION); | |
| assert.equal(draft1.patch_set, 3 as RevisionPatchSetNum); | |
| }); | |
| test('should be on parent if right and base is PARENT', () => { | |
| element.patchRange = createPatchRange(); | |
| element.dispatchEvent( | |
| new CustomEvent('create-comment', { | |
| detail: { | |
| side: Side.LEFT, | |
| }, | |
| }) | |
| ); | |
| assert.equal(addDraftSpy.callCount, 1); | |
| const draft1: DraftInfo = addDraftSpy.lastCall.firstArg; | |
| assert.equal(draft1.side, CommentSide.PARENT); | |
| assert.equal(draft1.patch_set, 1 as RevisionPatchSetNum); | |
| }); | |
| test('should be on parent if right and base negative', () => { | |
| element.patchRange = createPatchRange(-2, 3); | |
| element.dispatchEvent( | |
| new CustomEvent('create-comment', { | |
| detail: { | |
| side: Side.LEFT, | |
| }, | |
| }) | |
| ); | |
| assert.equal(addDraftSpy.callCount, 1); | |
| const draft1: DraftInfo = addDraftSpy.lastCall.firstArg; | |
| assert.equal(draft1.side, CommentSide.PARENT); | |
| assert.equal(draft1.patch_set, 3 as RevisionPatchSetNum); | |
| assert.equal(draft1.parent, 2); | |
| }); | |
| test('should not be on parent otherwise', () => { | |
| element.patchRange = createPatchRange(2, 3); | |
| element.dispatchEvent( | |
| new CustomEvent('create-comment', { | |
| detail: { | |
| side: Side.LEFT, | |
| }, | |
| }) | |
| ); | |
| assert.equal(addDraftSpy.callCount, 1); | |
| const draft1: DraftInfo = addDraftSpy.lastCall.firstArg; | |
| assert.equal(draft1.side, CommentSide.REVISION); | |
| assert.equal(draft1.patch_set, 2 as RevisionPatchSetNum); | |
| }); | |
| test( | |
| 'thread should use old file path if first created ' + | |
| 'on patch set (left) before renaming', | |
| async () => { | |
| element.patchRange = createPatchRange(2, 3); | |
| element.file = {basePath: 'file_renamed.txt', path: element.path ?? ''}; | |
| await element.updateComplete; | |
| element.dispatchEvent( | |
| new CustomEvent('create-comment', { | |
| detail: { | |
| side: Side.LEFT, | |
| path: '/p', | |
| }, | |
| }) | |
| ); | |
| assert.equal(addDraftSpy.callCount, 1); | |
| const draft1: DraftInfo = addDraftSpy.lastCall.firstArg; | |
| assert.equal(draft1.side, CommentSide.REVISION); | |
| assert.equal(draft1.patch_set, 2 as RevisionPatchSetNum); | |
| assert.equal(draft1.path, element.file.basePath); | |
| } | |
| ); | |
| test( | |
| 'thread should use new file path if first created ' + | |
| 'on patch set (right) after renaming', | |
| async () => { | |
| element.patchRange = createPatchRange(2, 3); | |
| element.file = {basePath: 'file_renamed.txt', path: element.path ?? ''}; | |
| await element.updateComplete; | |
| element.dispatchEvent( | |
| new CustomEvent('create-comment', { | |
| detail: { | |
| side: Side.RIGHT, | |
| path: '/p', | |
| }, | |
| }) | |
| ); | |
| assert.equal(addDraftSpy.callCount, 1); | |
| const draft1: DraftInfo = addDraftSpy.lastCall.firstArg; | |
| assert.equal(draft1.side, CommentSide.REVISION); | |
| assert.equal(draft1.patch_set, 3 as RevisionPatchSetNum); | |
| assert.equal(draft1.path, element.file.path); | |
| } | |
| ); | |
| test( | |
| 'thread should use new file path if first created ' + | |
| 'on patch set (left) but is base', | |
| async () => { | |
| element.patchRange = createPatchRange(); | |
| element.file = {basePath: 'file_renamed.txt', path: element.path ?? ''}; | |
| await element.updateComplete; | |
| element.dispatchEvent( | |
| new CustomEvent('create-comment', { | |
| detail: { | |
| side: Side.LEFT, | |
| path: '/p', | |
| }, | |
| }) | |
| ); | |
| assert.equal(addDraftSpy.callCount, 1); | |
| const draft1: DraftInfo = addDraftSpy.lastCall.firstArg; | |
| assert.equal(draft1.side, CommentSide.PARENT); | |
| assert.equal(draft1.patch_set, 1 as RevisionPatchSetNum); | |
| assert.equal(draft1.path, element.file.path); | |
| } | |
| ); | |
| test('cannot create thread on an edit', () => { | |
| const alertSpy = sinon.spy(); | |
| element.addEventListener('show-alert', alertSpy); | |
| const diffSide = Side.RIGHT; | |
| element.patchRange = { | |
| basePatchNum: 3 as BasePatchSetNum, | |
| patchNum: EDIT, | |
| }; | |
| element.dispatchEvent( | |
| new CustomEvent('create-comment', { | |
| detail: { | |
| side: diffSide, | |
| path: '/p', | |
| }, | |
| }) | |
| ); | |
| assert.isFalse(addDraftSpy.called); | |
| assert.isTrue(alertSpy.called); | |
| }); | |
| test('cannot create thread on an edit base', () => { | |
| const alertSpy = sinon.spy(); | |
| element.addEventListener('show-alert', alertSpy); | |
| const diffSide = Side.LEFT; | |
| element.patchRange = { | |
| basePatchNum: PARENT, | |
| patchNum: EDIT, | |
| }; | |
| element.dispatchEvent( | |
| new CustomEvent('create-comment', { | |
| detail: { | |
| side: diffSide, | |
| path: '/p', | |
| }, | |
| }) | |
| ); | |
| assert.isFalse(addDraftSpy.called); | |
| assert.isTrue(alertSpy.called); | |
| }); | |
| }); | |
| suite('syntax layer with syntax_highlighting on', async () => { | |
| setup(async () => { | |
| const prefs = { | |
| ...createDefaultDiffPrefs(), | |
| line_length: 10, | |
| show_tabs: true, | |
| tab_size: 4, | |
| context: -1, | |
| syntax_highlighting: true, | |
| }; | |
| element.patchRange = createPatchRange(); | |
| element.prefs = prefs; | |
| element.changeNum = 123 as NumericChangeId; | |
| element.change = createChange(); | |
| element.path = 'some/path'; | |
| }); | |
| test('gr-diff-host provides syntax highlighting layer', async () => { | |
| getDiffRestApiStub.returns(Promise.resolve(createDiff())); | |
| await element.updateComplete; | |
| assertIsDefined(element.diffElement); | |
| assertIsDefined(element.diffElement.layers); | |
| assert.equal(element.diffElement.layers[1], element.syntaxLayer); | |
| }); | |
| test('rendering normal-sized diff does not disable syntax', async () => { | |
| element.diff = createDiff(); | |
| getDiffRestApiStub.returns(Promise.resolve(element.diff)); | |
| await element.updateComplete; | |
| assert.isTrue(element.syntaxLayer.enabled); | |
| }); | |
| test('starts syntax layer processing on render event', async () => { | |
| const stub = sinon | |
| .stub(element.syntaxLayer, 'process') | |
| .returns(Promise.resolve()); | |
| getDiffRestApiStub.returns(Promise.resolve(createDiff())); | |
| await element.reload(); | |
| element.dispatchEvent( | |
| new CustomEvent('render', {bubbles: true, composed: true}) | |
| ); | |
| assert.isTrue(stub.called); | |
| }); | |
| }); | |
| suite('syntax layer with syntax_highlighting off', () => { | |
| setup(async () => { | |
| const prefs = { | |
| ...createDefaultDiffPrefs(), | |
| line_length: 10, | |
| show_tabs: true, | |
| tab_size: 4, | |
| context: -1, | |
| syntax_highlighting: false, | |
| }; | |
| element.patchRange = createPatchRange(); | |
| element.change = createChange(); | |
| element.prefs = prefs; | |
| }); | |
| test('gr-diff-host provides syntax highlighting layer', async () => { | |
| await element.waitForReloadToRender(); | |
| assertIsDefined(element.diffElement); | |
| assertIsDefined(element.diffElement.layers); | |
| assert.equal(element.diffElement.layers[1], element.syntaxLayer); | |
| }); | |
| test('syntax layer should be disabled', async () => { | |
| await element.waitForReloadToRender(); | |
| assert.isFalse(element.syntaxLayer.enabled); | |
| }); | |
| test('still disabled for large diff', async () => { | |
| getDiffRestApiStub.callsFake(() => | |
| Promise.resolve({ | |
| ...createDiff(), | |
| content: [ | |
| { | |
| a: ['*'.repeat(501)], | |
| }, | |
| ], | |
| }) | |
| ); | |
| await element.waitForReloadToRender(); | |
| assert.isFalse(element.syntaxLayer.enabled); | |
| }); | |
| }); | |
| suite('coverage layer', () => { | |
| let coverageProviderStub: SinonStub; | |
| const exampleRanges = [ | |
| { | |
| type: CoverageType.COVERED, | |
| side: Side.RIGHT, | |
| code_range: { | |
| start_line: 1, | |
| end_line: 2, | |
| }, | |
| }, | |
| { | |
| type: CoverageType.NOT_COVERED, | |
| side: Side.RIGHT, | |
| code_range: { | |
| start_line: 3, | |
| end_line: 4, | |
| }, | |
| }, | |
| ]; | |
| setup(async () => { | |
| element.prefs = { | |
| ...createDefaultDiffPrefs(), | |
| line_length: 10, | |
| show_tabs: true, | |
| tab_size: 4, | |
| context: -1, | |
| }; | |
| await element.updateComplete; | |
| coverageProviderStub = sinon | |
| .stub() | |
| .returns(Promise.resolve(exampleRanges)); | |
| getDiffRestApiStub.returns( | |
| Promise.resolve({ | |
| ...createDiff(), | |
| content: [{a: ['foo']}], | |
| }) | |
| ); | |
| testResolver(pluginLoaderToken).pluginsModel.coverageRegister({ | |
| pluginName: 'test-coverage-plugin', | |
| provider: coverageProviderStub, | |
| }); | |
| await element.reload(); | |
| }); | |
| test('provider is called with appropriate params', async () => { | |
| element.patchRange = createPatchRange(1, 3); | |
| await element.updateComplete; | |
| await element.reload(); | |
| await element.waitForReloadToRender(); | |
| assert.isTrue( | |
| coverageProviderStub.calledWithExactly( | |
| 123, | |
| 'some/path', | |
| 1, | |
| 3, | |
| element.change | |
| ) | |
| ); | |
| }); | |
| test('provider is called with appropriate params - special patchset values', async () => { | |
| element.patchRange = createPatchRange(); | |
| await element.waitForReloadToRender(); | |
| assert.isTrue( | |
| coverageProviderStub.calledWithExactly( | |
| 123, | |
| 'some/path', | |
| undefined, | |
| 1, | |
| element.change | |
| ) | |
| ); | |
| }); | |
| }); | |
| suite('trailing newlines', () => { | |
| setup(() => {}); | |
| suite('lastChunkForSide', () => { | |
| test('deltas', () => { | |
| const diff: DiffInfo = { | |
| ...createDiff(), | |
| content: [ | |
| {a: ['foo', 'bar'], b: ['baz']}, | |
| {ab: ['foo', 'bar', 'baz']}, | |
| {b: ['foo']}, | |
| ], | |
| }; | |
| assert.equal(element.lastChunkForSide(diff, false), diff.content[2]); | |
| assert.equal(element.lastChunkForSide(diff, true), diff.content[1]); | |
| diff.content.push({a: ['foo'], b: ['bar']}); | |
| assert.equal(element.lastChunkForSide(diff, false), diff.content[3]); | |
| assert.equal(element.lastChunkForSide(diff, true), diff.content[3]); | |
| }); | |
| test('addition with a undefined', () => { | |
| const diff: DiffInfo = { | |
| ...createDiff(), | |
| content: [{b: ['foo', 'bar', 'baz']}], | |
| }; | |
| assert.equal(element.lastChunkForSide(diff, false), diff.content[0]); | |
| assert.isNull(element.lastChunkForSide(diff, true)); | |
| }); | |
| test('addition with a empty', () => { | |
| const diff: DiffInfo = { | |
| ...createDiff(), | |
| content: [{a: [], b: ['foo', 'bar', 'baz']}], | |
| }; | |
| assert.equal(element.lastChunkForSide(diff, false), diff.content[0]); | |
| assert.isNull(element.lastChunkForSide(diff, true)); | |
| }); | |
| test('deletion with b undefined', () => { | |
| const diff: DiffInfo = { | |
| ...createDiff(), | |
| content: [{a: ['foo', 'bar', 'baz']}], | |
| }; | |
| assert.isNull(element.lastChunkForSide(diff, false)); | |
| assert.equal(element.lastChunkForSide(diff, true), diff.content[0]); | |
| }); | |
| test('deletion with b empty', () => { | |
| const diff: DiffInfo = { | |
| ...createDiff(), | |
| content: [{a: ['foo', 'bar', 'baz'], b: []}], | |
| }; | |
| assert.isNull(element.lastChunkForSide(diff, false)); | |
| assert.equal(element.lastChunkForSide(diff, true), diff.content[0]); | |
| }); | |
| test('empty', () => { | |
| const diff: DiffInfo = {...createDiff(), content: []}; | |
| assert.isNull(element.lastChunkForSide(diff, false)); | |
| assert.isNull(element.lastChunkForSide(diff, true)); | |
| }); | |
| }); | |
| suite('hasTrailingNewlines', () => { | |
| test('shared no trailing', () => { | |
| const diff = undefined; | |
| sinon.stub(element, 'lastChunkForSide').returns({ab: ['foo', 'bar']}); | |
| assert.isFalse(element.hasTrailingNewlines(diff, false)); | |
| assert.isFalse(element.hasTrailingNewlines(diff, true)); | |
| }); | |
| test('delta trailing in right', () => { | |
| const diff = undefined; | |
| sinon | |
| .stub(element, 'lastChunkForSide') | |
| .returns({a: ['foo', 'bar'], b: ['baz', '']}); | |
| assert.isTrue(element.hasTrailingNewlines(diff, false)); | |
| assert.isFalse(element.hasTrailingNewlines(diff, true)); | |
| }); | |
| test('addition', () => { | |
| const diff: DiffInfo | undefined = undefined; | |
| sinon | |
| .stub(element, 'lastChunkForSide') | |
| .callsFake((_: DiffInfo | undefined, leftSide: boolean) => { | |
| if (leftSide) { | |
| return null; | |
| } | |
| return {b: ['foo', '']}; | |
| }); | |
| assert.isTrue(element.hasTrailingNewlines(diff, false)); | |
| assert.isNull(element.hasTrailingNewlines(diff, true)); | |
| }); | |
| test('deletion', () => { | |
| const diff: DiffInfo | undefined = undefined; | |
| sinon | |
| .stub(element, 'lastChunkForSide') | |
| .callsFake((_: DiffInfo | undefined, leftSide: boolean) => { | |
| if (!leftSide) { | |
| return null; | |
| } | |
| return {a: ['foo']}; | |
| }); | |
| assert.isNull(element.hasTrailingNewlines(diff, false)); | |
| assert.isFalse(element.hasTrailingNewlines(diff, true)); | |
| }); | |
| }); | |
| }); | |
| suite('computeLayersWithPlugins and token highlighting', () => { | |
| let getPreferencesStub: sinon.SinonStub; | |
| setup(() => { | |
| getPreferencesStub = stubRestApi('getPreferences'); | |
| }); | |
| test('initLayers sets enableTokenHighlight correctly', async () => { | |
| getPreferencesStub.returns( | |
| Promise.resolve({disable_token_highlighting: true}) | |
| ); | |
| await element.initLayers(); | |
| assert.isFalse(element['enableTokenHighlight']); | |
| getPreferencesStub.returns( | |
| Promise.resolve({disable_token_highlighting: false}) | |
| ); | |
| await element.initLayers(); | |
| assert.isTrue(element['enableTokenHighlight']); | |
| getPreferencesStub.returns(Promise.resolve(undefined)); | |
| await element.initLayers(); | |
| assert.isTrue(element['enableTokenHighlight']); | |
| }); | |
| test('clear resets enableTokenHighlight', async () => { | |
| getPreferencesStub.returns( | |
| Promise.resolve({disable_token_highlighting: true}) | |
| ); | |
| await element.initLayers(); | |
| assert.isFalse(element['enableTokenHighlight']); | |
| element.clear(); | |
| assert.isUndefined(element['enableTokenHighlight']); | |
| }); | |
| test('computeLayersWithPlugins sets enableTokenHighlight if undefined', async () => { | |
| getPreferencesStub.returns( | |
| Promise.resolve({disable_token_highlighting: true}) | |
| ); | |
| element['enableTokenHighlight'] = undefined; | |
| element['layersComputedWithPlugins'] = false; | |
| await element['computeLayersWithPlugins'](); | |
| assert.isFalse(element['enableTokenHighlight']); | |
| assert.isTrue(element['layersComputedWithPlugins']); | |
| assert.isTrue(getPreferencesStub.calledOnce); | |
| }); | |
| test('computeLayersWithPlugins does not fetch preferences if enableTokenHighlight is already set', async () => { | |
| getPreferencesStub.returns( | |
| Promise.resolve({disable_token_highlighting: true}) | |
| ); | |
| element['enableTokenHighlight'] = true; | |
| element['layersComputedWithPlugins'] = false; | |
| await element['computeLayersWithPlugins'](); | |
| assert.isTrue(element['enableTokenHighlight']); | |
| assert.isTrue(element['layersComputedWithPlugins']); | |
| assert.isFalse(getPreferencesStub.called); | |
| }); | |
| test('pluginsLoaded triggers computeLayersWithPlugins', async () => { | |
| // @ts-expect-error | |
| const computeSpy = sinon.spy(element, 'computeLayersWithPlugins'); | |
| const pluginsModel = testResolver(pluginLoaderToken).pluginsModel; | |
| pluginsModel.updateState({pluginsLoaded: false}); | |
| await element.updateComplete; | |
| pluginsModel.updateState({pluginsLoaded: true}); | |
| await new Promise(resolve => setTimeout(resolve, 0)); | |
| assert.isTrue(computeSpy.called); | |
| }); | |
| }); | |
| suite('revert change in edit mode', () => { | |
| setup(() => { | |
| userModel.setAccount(account); | |
| }); | |
| test('is_edit_mode is passed in renderPrefs only in edit mode', async () => { | |
| element.patchRange = createPatchRange(); | |
| await element.updateComplete; | |
| const grDiff = element.shadowRoot?.querySelector('gr-diff'); | |
| assert.isFalse(grDiff?.renderPrefs?.is_edit_mode); | |
| element.patchRange = { | |
| ...createPatchRange(), | |
| patchNum: EDIT, | |
| }; | |
| await element.updateComplete; | |
| assert.isTrue(grDiff?.renderPrefs?.is_edit_mode); | |
| }); | |
| test('handleRevertDelta applies fix suggestion and reloads diff', async () => { | |
| const applyFixStub = stubRestApi('applyFixSuggestion').returns( | |
| Promise.resolve(new Response('', {status: 200})) | |
| ); | |
| const reloadStub = sinon.stub(element, 'reload').resolves(); | |
| element.patchRange = { | |
| ...createPatchRange(), | |
| patchNum: EDIT, | |
| }; | |
| element.latestPatchNum = 1 as PatchSetNumber; | |
| element.path = 'foo.ts'; | |
| element.changeNum = 42 as NumericChangeId; | |
| const removeLine = new GrDiffLine(GrDiffLineType.REMOVE, 10, 0); | |
| removeLine.text = 'old code'; | |
| const addLine = new GrDiffLine(GrDiffLineType.ADD, 0, 10); | |
| addLine.text = 'new code'; | |
| const group = new GrDiffGroup({ | |
| type: GrDiffGroupType.DELTA, | |
| lines: [removeLine, addLine], | |
| }); | |
| const setUrlStub = sinon.stub(testResolver(navigationToken), 'setUrl'); | |
| const reportStub = sinon.stub(element.reporting, 'reportInteraction'); | |
| const timeEndStub = sinon.stub(element.reporting, 'timeEnd'); | |
| await element.handleRevertDelta(group); | |
| assert.isTrue(setUrlStub.calledOnce); | |
| assert.include(setUrlStub.firstCall.args[0], '/+/42/edit'); | |
| assert.notInclude(setUrlStub.firstCall.args[0], '..edit'); | |
| assert.notInclude(setUrlStub.firstCall.args[0], 'forceReload=true'); | |
| assert.isTrue( | |
| reportStub.calledWith(Interaction.REVERT_DELTA_CLICKED, { | |
| path: 'foo.ts', | |
| }) | |
| ); | |
| assert.isTrue( | |
| timeEndStub.calledWith( | |
| Timing.REVERT_DELTA_LOAD, | |
| sinon.match({success: true}) | |
| ) | |
| ); | |
| assert.isTrue(applyFixStub.calledOnce); | |
| assert.equal(applyFixStub.firstCall.args[0], 42 as NumericChangeId); | |
| assert.equal(applyFixStub.firstCall.args[1], 1 as RevisionPatchSetNum); | |
| assert.isUndefined(applyFixStub.firstCall.args[3]); | |
| assert.equal(applyFixStub.firstCall.args[4], throwingErrorCallback); | |
| assert.deepEqual(applyFixStub.firstCall.args[2], [ | |
| { | |
| path: 'foo.ts', | |
| range: { | |
| start_line: 10, | |
| start_character: 0, | |
| end_line: 10, | |
| end_character: 8, | |
| }, | |
| replacement: 'old code', | |
| }, | |
| ]); | |
| assert.isTrue(reloadStub.lastCall.calledWith(true)); | |
| }); | |
| test('is_edit_mode is passed in renderPrefs when editMode is true', async () => { | |
| element.patchRange = createPatchRange(); | |
| element.latestPatchNum = 1 as PatchSetNumber; | |
| element.editMode = false; | |
| await element.updateComplete; | |
| let grDiff = element.shadowRoot?.querySelector('gr-diff'); | |
| assert.isFalse(grDiff?.renderPrefs?.is_edit_mode); | |
| element.editMode = true; | |
| await element.updateComplete; | |
| grDiff = element.shadowRoot?.querySelector('gr-diff'); | |
| assert.isTrue(grDiff?.renderPrefs?.is_edit_mode); | |
| }); | |
| test('handleRevertDelta applies fix suggestion when in editMode with numeric patchset', async () => { | |
| const applyFixStub = stubRestApi('applyFixSuggestion').returns( | |
| Promise.resolve(new Response('', {status: 200})) | |
| ); | |
| sinon.stub(element, 'reload').resolves(); | |
| const setUrlStub = sinon.stub(testResolver(navigationToken), 'setUrl'); | |
| element.patchRange = createPatchRange(); // numeric patchNum: 1 | |
| element.latestPatchNum = 1 as PatchSetNumber; | |
| element.editMode = true; | |
| element.path = 'foo.ts'; | |
| element.changeNum = 42 as NumericChangeId; | |
| const removeLine = new GrDiffLine(GrDiffLineType.REMOVE, 10, 0); | |
| removeLine.text = 'old code'; | |
| const addLine = new GrDiffLine(GrDiffLineType.ADD, 0, 10); | |
| addLine.text = 'new code'; | |
| const group = new GrDiffGroup({ | |
| type: GrDiffGroupType.DELTA, | |
| lines: [removeLine, addLine], | |
| }); | |
| await element.handleRevertDelta(group); | |
| assert.isTrue(setUrlStub.calledOnce); | |
| assert.include(setUrlStub.firstCall.args[0], '/+/42/edit'); | |
| assert.notInclude(setUrlStub.firstCall.args[0], '..edit'); | |
| assert.include(setUrlStub.firstCall.args[0], 'forceReload=true'); | |
| assert.isTrue(applyFixStub.calledOnce); | |
| assert.equal(applyFixStub.firstCall.args[0], 42 as NumericChangeId); | |
| assert.equal(applyFixStub.firstCall.args[1], 1 as RevisionPatchSetNum); | |
| }); | |
| test('handleRevertDelta applies fix suggestion when patchNum is EDIT', async () => { | |
| const applyFixStub = stubRestApi('applyFixSuggestion').returns( | |
| Promise.resolve(new Response('', {status: 200})) | |
| ); | |
| sinon.stub(element, 'reload').resolves(); | |
| const setUrlStub = sinon.stub(testResolver(navigationToken), 'setUrl'); | |
| element.patchRange = { | |
| ...createPatchRange(), | |
| patchNum: EDIT, | |
| }; | |
| element.latestPatchNum = 3 as PatchSetNumber; | |
| element.editMode = true; | |
| element.path = 'foo.ts'; | |
| element.changeNum = 42 as NumericChangeId; | |
| const removeLine = new GrDiffLine(GrDiffLineType.REMOVE, 10, 0); | |
| removeLine.text = 'old code'; | |
| const addLine = new GrDiffLine(GrDiffLineType.ADD, 0, 10); | |
| addLine.text = 'new code'; | |
| const group = new GrDiffGroup({ | |
| type: GrDiffGroupType.DELTA, | |
| lines: [removeLine, addLine], | |
| }); | |
| await element.handleRevertDelta(group); | |
| assert.isTrue(setUrlStub.calledOnce); | |
| assert.include(setUrlStub.firstCall.args[0], '/+/42/edit'); | |
| assert.notInclude(setUrlStub.firstCall.args[0], 'forceReload=true'); | |
| assert.isTrue(applyFixStub.calledOnce); | |
| assert.equal(applyFixStub.firstCall.args[0], 42 as NumericChangeId); | |
| assert.equal(applyFixStub.firstCall.args[1], 3 as RevisionPatchSetNum); | |
| assert.isUndefined(applyFixStub.firstCall.args[3]); | |
| }); | |
| test('handleRevertDelta resolves base patchset from edit revision', async () => { | |
| const applyFixStub = stubRestApi('applyFixSuggestion').returns( | |
| Promise.resolve(new Response('', {status: 200})) | |
| ); | |
| sinon.stub(element, 'reload').resolves(); | |
| const setUrlStub = sinon.stub(testResolver(navigationToken), 'setUrl'); | |
| element.patchRange = { | |
| ...createPatchRange(), | |
| patchNum: EDIT, | |
| }; | |
| element.change = { | |
| ...createChange(), | |
| revisions: { | |
| r1: createRevision(1), | |
| r2: createRevision(2), | |
| rEdit: createEditRevision(2) as unknown as RevisionInfo, | |
| }, | |
| }; | |
| element.latestPatchNum = 3 as PatchSetNumber; | |
| element.editMode = true; | |
| element.path = 'foo.ts'; | |
| element.changeNum = 42 as NumericChangeId; | |
| const removeLine = new GrDiffLine(GrDiffLineType.REMOVE, 10, 0); | |
| removeLine.text = 'old code'; | |
| const addLine = new GrDiffLine(GrDiffLineType.ADD, 0, 10); | |
| addLine.text = 'new code'; | |
| const group = new GrDiffGroup({ | |
| type: GrDiffGroupType.DELTA, | |
| lines: [removeLine, addLine], | |
| }); | |
| await element.handleRevertDelta(group); | |
| assert.isTrue(setUrlStub.calledOnce); | |
| assert.notInclude(setUrlStub.firstCall.args[0], 'forceReload=true'); | |
| assert.isTrue(applyFixStub.calledOnce); | |
| assert.equal(applyFixStub.firstCall.args[0], 42 as NumericChangeId); | |
| assert.equal(applyFixStub.firstCall.args[1], 2 as RevisionPatchSetNum); | |
| }); | |
| test('handleRevertDelta calls onComplete callback on success', async () => { | |
| stubRestApi('applyFixSuggestion').returns( | |
| Promise.resolve(new Response('', {status: 200})) | |
| ); | |
| sinon.stub(element, 'reload').resolves(); | |
| sinon.stub(testResolver(navigationToken), 'setUrl'); | |
| element.patchRange = { | |
| ...createPatchRange(), | |
| patchNum: EDIT, | |
| }; | |
| element.latestPatchNum = 1 as PatchSetNumber; | |
| element.editMode = true; | |
| element.path = 'foo.ts'; | |
| element.changeNum = 42 as NumericChangeId; | |
| const removeLine = new GrDiffLine(GrDiffLineType.REMOVE, 10, 0); | |
| removeLine.text = 'old code'; | |
| const addLine = new GrDiffLine(GrDiffLineType.ADD, 0, 10); | |
| addLine.text = 'new code'; | |
| const group = new GrDiffGroup({ | |
| type: GrDiffGroupType.DELTA, | |
| lines: [removeLine, addLine], | |
| }); | |
| const onCompleteSpy = sinon.spy(); | |
| await element.handleRevertDelta(group, onCompleteSpy); | |
| assert.isTrue(onCompleteSpy.calledOnce); | |
| }); | |
| test('handleRevertDelta calls onComplete callback on failure', async () => { | |
| stubRestApi('applyFixSuggestion').returns( | |
| Promise.reject(new Error('Network error')) | |
| ); | |
| sinon.stub(element, 'reload').resolves(); | |
| element.patchRange = { | |
| ...createPatchRange(), | |
| patchNum: EDIT, | |
| }; | |
| element.latestPatchNum = 1 as PatchSetNumber; | |
| element.editMode = true; | |
| element.path = 'foo.ts'; | |
| element.changeNum = 42 as NumericChangeId; | |
| const removeLine = new GrDiffLine(GrDiffLineType.REMOVE, 10, 0); | |
| removeLine.text = 'old code'; | |
| const addLine = new GrDiffLine(GrDiffLineType.ADD, 0, 10); | |
| addLine.text = 'new code'; | |
| const group = new GrDiffGroup({ | |
| type: GrDiffGroupType.DELTA, | |
| lines: [removeLine, addLine], | |
| }); | |
| const onCompleteSpy = sinon.spy(); | |
| await element.handleRevertDelta(group, onCompleteSpy); | |
| assert.isTrue(onCompleteSpy.calledOnce); | |
| }); | |
| test('handleRevertDelta does not apply fix if not in edit mode', async () => { | |
| const applyFixStub = stubRestApi('applyFixSuggestion'); | |
| element.patchRange = createPatchRange(); | |
| element.editMode = false; | |
| element.path = 'foo.ts'; | |
| element.changeNum = 42 as NumericChangeId; | |
| const removeLine = new GrDiffLine(GrDiffLineType.REMOVE, 10, 0); | |
| removeLine.text = 'old code'; | |
| const addLine = new GrDiffLine(GrDiffLineType.ADD, 0, 10); | |
| addLine.text = 'new code'; | |
| const group = new GrDiffGroup({ | |
| type: GrDiffGroupType.DELTA, | |
| lines: [removeLine, addLine], | |
| }); | |
| await element.handleRevertDelta(group); | |
| assert.isFalse(applyFixStub.called); | |
| }); | |
| test('handleRevertDelta ignores concurrent revert calls when already reverting', async () => { | |
| let resolveApplyFix: (res: Response) => void; | |
| const applyFixPromise = new Promise<Response>(resolve => { | |
| resolveApplyFix = resolve; | |
| }); | |
| const applyFixStub = | |
| stubRestApi('applyFixSuggestion').returns(applyFixPromise); | |
| sinon.stub(element, 'reload').resolves(); | |
| element.patchRange = { | |
| ...createPatchRange(), | |
| patchNum: EDIT, | |
| }; | |
| element.latestPatchNum = 1 as PatchSetNumber; | |
| element.editMode = true; | |
| element.path = 'foo.ts'; | |
| element.changeNum = 42 as NumericChangeId; | |
| const removeLine = new GrDiffLine(GrDiffLineType.REMOVE, 10, 0); | |
| removeLine.text = 'old code'; | |
| const addLine = new GrDiffLine(GrDiffLineType.ADD, 0, 10); | |
| addLine.text = 'new code'; | |
| const group = new GrDiffGroup({ | |
| type: GrDiffGroupType.DELTA, | |
| lines: [removeLine, addLine], | |
| }); | |
| const firstCall = element.handleRevertDelta(group); | |
| const secondCall = element.handleRevertDelta(group); | |
| resolveApplyFix!(new Response('', {status: 200})); | |
| await Promise.all([firstCall, secondCall]); | |
| assert.isTrue(applyFixStub.calledOnce); | |
| }); | |
| test('is_edit_mode is false when in editMode but viewing older patchset', async () => { | |
| element.patchRange = { | |
| ...createPatchRange(), | |
| patchNum: 1 as RevisionPatchSetNum, | |
| }; | |
| element.latestPatchNum = 2 as PatchSetNumber; | |
| element.editMode = true; | |
| await element.updateComplete; | |
| const grDiff = element.shadowRoot?.querySelector('gr-diff'); | |
| assert.isFalse(grDiff?.renderPrefs?.is_edit_mode); | |
| assert.isFalse(element.isRevertAllowed()); | |
| }); | |
| test('is_edit_mode is false for commit message, merge list, and project.config', async () => { | |
| element.patchRange = { | |
| ...createPatchRange(), | |
| patchNum: EDIT, | |
| }; | |
| element.editMode = true; | |
| element.path = '/COMMIT_MSG'; | |
| await element.updateComplete; | |
| let grDiff = element.shadowRoot?.querySelector('gr-diff'); | |
| assert.isFalse(grDiff?.renderPrefs?.is_edit_mode); | |
| assert.isFalse(element.isRevertAllowed()); | |
| element.path = '/MERGE_LIST'; | |
| await element.updateComplete; | |
| grDiff = element.shadowRoot?.querySelector('gr-diff'); | |
| assert.isFalse(grDiff?.renderPrefs?.is_edit_mode); | |
| assert.isFalse(element.isRevertAllowed()); | |
| element.path = 'project.config'; | |
| await element.updateComplete; | |
| grDiff = element.shadowRoot?.querySelector('gr-diff'); | |
| assert.isFalse(grDiff?.renderPrefs?.is_edit_mode); | |
| assert.isFalse(element.isRevertAllowed()); | |
| }); | |
| test('is_edit_mode is false for refs/meta/config branch', async () => { | |
| element.patchRange = { | |
| ...createPatchRange(), | |
| patchNum: EDIT, | |
| }; | |
| element.editMode = true; | |
| element.path = 'groups'; | |
| element.change = { | |
| ...createChange(), | |
| branch: 'refs/meta/config' as BranchName, | |
| }; | |
| await element.updateComplete; | |
| const grDiff = element.shadowRoot?.querySelector('gr-diff'); | |
| assert.isFalse(grDiff?.renderPrefs?.is_edit_mode); | |
| assert.isFalse(element.isRevertAllowed()); | |
| }); | |
| test('is_edit_mode is false for binary and image diffs', async () => { | |
| element.patchRange = { | |
| ...createPatchRange(), | |
| patchNum: EDIT, | |
| }; | |
| element.editMode = true; | |
| element.path = 'image.png'; | |
| element.diff = { | |
| ...createDiff(), | |
| binary: true, | |
| }; | |
| await element.updateComplete; | |
| assert.isFalse(element.isRevertAllowed()); | |
| }); | |
| test('is_edit_mode is false for merged or abandoned changes', async () => { | |
| element.patchRange = { | |
| ...createPatchRange(), | |
| patchNum: EDIT, | |
| }; | |
| element.editMode = true; | |
| element.path = 'foo.ts'; | |
| element.change = { | |
| ...createChange(), | |
| status: ChangeStatus.MERGED, | |
| }; | |
| await element.updateComplete; | |
| assert.isFalse(element.isRevertAllowed()); | |
| element.change = { | |
| ...createChange(), | |
| status: ChangeStatus.ABANDONED, | |
| }; | |
| await element.updateComplete; | |
| assert.isFalse(element.isRevertAllowed()); | |
| }); | |
| test('isRevertAllowed is false when logged out', async () => { | |
| userModel.setAccount(undefined); | |
| element.patchRange = { | |
| ...createPatchRange(), | |
| patchNum: EDIT, | |
| }; | |
| element.editMode = true; | |
| element.path = 'foo.ts'; | |
| await element.updateComplete; | |
| assert.isFalse(element.isRevertAllowed()); | |
| }); | |
| test('isRevertAllowed is false when basePatchNum is not a parent', async () => { | |
| element.patchRange = { | |
| basePatchNum: 1 as BasePatchSetNum, | |
| patchNum: EDIT, | |
| }; | |
| element.editMode = true; | |
| element.path = 'foo.ts'; | |
| await element.updateComplete; | |
| assert.isFalse(element.isRevertAllowed()); | |
| }); | |
| test('handleRevertDelta uses saveChangeEdit when in EDIT mode with groups loaded', async () => { | |
| const applyFixStub = stubRestApi('applyFixSuggestion'); | |
| const saveEditStub = stubRestApi('saveChangeEdit').returns( | |
| Promise.resolve(new Response(null, {status: 204})) | |
| ); | |
| sinon.stub(element, 'reload').resolves(); | |
| sinon.stub(testResolver(navigationToken), 'setUrl'); | |
| element.patchRange = { | |
| ...createPatchRange(), | |
| patchNum: EDIT, | |
| }; | |
| element.latestPatchNum = 1 as PatchSetNumber; | |
| element.editMode = true; | |
| element.path = 'foo.ts'; | |
| element.changeNum = 42 as NumericChangeId; | |
| await element.updateComplete; | |
| const hunk1Remove = new GrDiffLine(GrDiffLineType.REMOVE, 1, 0); | |
| hunk1Remove.text = 'const a = 1;'; | |
| const hunk1Add = new GrDiffLine(GrDiffLineType.ADD, 0, 1); | |
| hunk1Add.text = 'const a = 2;'; | |
| const hunk1Group = new GrDiffGroup({ | |
| type: GrDiffGroupType.DELTA, | |
| lines: [hunk1Remove, hunk1Add], | |
| }); | |
| const hunk2Remove = new GrDiffLine(GrDiffLineType.REMOVE, 2, 0); | |
| hunk2Remove.text = 'const b = 1;'; | |
| const hunk2Add = new GrDiffLine(GrDiffLineType.ADD, 0, 2); | |
| hunk2Add.text = 'const b = 2;'; | |
| const hunk2Group = new GrDiffGroup({ | |
| type: GrDiffGroupType.DELTA, | |
| lines: [hunk2Remove, hunk2Add], | |
| }); | |
| assertIsDefined(element.diffElement); | |
| element.diffElement.groups = [hunk1Group, hunk2Group]; | |
| await element.handleRevertDelta(hunk2Group); | |
| assert.isFalse(applyFixStub.called); | |
| assert.isTrue(saveEditStub.calledOnce); | |
| assert.equal(saveEditStub.firstCall.args[0], 42 as NumericChangeId); | |
| assert.equal(saveEditStub.firstCall.args[1], 'foo.ts'); | |
| assert.equal( | |
| saveEditStub.firstCall.args[2], | |
| 'const a = 2;\nconst b = 1;' | |
| ); | |
| }); | |
| test('handleRevertDelta uses deleteFileInChangeEdit when reverting only delta of ADDED file', async () => { | |
| const applyFixStub = stubRestApi('applyFixSuggestion'); | |
| const deleteFileStub = stubRestApi('deleteFileInChangeEdit').returns( | |
| Promise.resolve(new Response(null, {status: 204})) | |
| ); | |
| sinon.stub(element, 'reload').resolves(); | |
| sinon.stub(testResolver(navigationToken), 'setUrl'); | |
| element.patchRange = { | |
| ...createPatchRange(), | |
| patchNum: EDIT, | |
| }; | |
| element.latestPatchNum = 1 as PatchSetNumber; | |
| element.editMode = true; | |
| element.path = 'added.ts'; | |
| element.changeNum = 42 as NumericChangeId; | |
| element.diff = { | |
| ...createDiff(), | |
| change_type: 'ADDED', | |
| }; | |
| await element.updateComplete; | |
| const addLine = new GrDiffLine(GrDiffLineType.ADD, 0, 1); | |
| addLine.text = 'new file content'; | |
| const group = new GrDiffGroup({ | |
| type: GrDiffGroupType.DELTA, | |
| lines: [addLine], | |
| }); | |
| assertIsDefined(element.diffElement); | |
| element.diffElement.groups = [group]; | |
| await element.handleRevertDelta(group); | |
| assert.isFalse(applyFixStub.called); | |
| assert.isTrue(deleteFileStub.calledOnce); | |
| assert.equal(deleteFileStub.firstCall.args[0], 42 as NumericChangeId); | |
| assert.equal(deleteFileStub.firstCall.args[1], 'added.ts'); | |
| }); | |
| test('handleRevertDelta falls back to saveChangeEdit when applyFixSuggestion fails', async () => { | |
| const applyFixStub = stubRestApi('applyFixSuggestion').callsFake(() => | |
| Promise.reject( | |
| new Error('Error 409: Cannot calculate fix replacement for range') | |
| ) | |
| ); | |
| const saveEditStub = stubRestApi('saveChangeEdit').returns( | |
| Promise.resolve(new Response(null, {status: 204})) | |
| ); | |
| sinon.stub(element, 'reload').resolves(); | |
| sinon.stub(testResolver(navigationToken), 'setUrl'); | |
| element.patchRange = createPatchRange(undefined, 1); | |
| element.latestPatchNum = 1 as PatchSetNumber; | |
| element.editMode = true; | |
| element.path = 'foo.ts'; | |
| element.changeNum = 42 as NumericChangeId; | |
| await element.updateComplete; | |
| const removeLine = new GrDiffLine(GrDiffLineType.REMOVE, 1, 0); | |
| removeLine.text = 'old code'; | |
| const addLine = new GrDiffLine(GrDiffLineType.ADD, 0, 1); | |
| addLine.text = 'new code'; | |
| const group = new GrDiffGroup({ | |
| type: GrDiffGroupType.DELTA, | |
| lines: [removeLine, addLine], | |
| }); | |
| assertIsDefined(element.diffElement); | |
| element.diffElement.groups = [group]; | |
| await element.handleRevertDelta(group); | |
| assert.isTrue(applyFixStub.calledOnce); | |
| assert.isTrue(saveEditStub.calledOnce); | |
| assert.equal(saveEditStub.firstCall.args[0], 42 as NumericChangeId); | |
| assert.equal(saveEditStub.firstCall.args[1], 'foo.ts'); | |
| assert.equal(saveEditStub.firstCall.args[2], 'old code'); | |
| }); | |
| test('handleRevertDelta logs telemetry when fix suggestion cannot be computed', async () => { | |
| const reportInteractionStub = sinon.stub( | |
| element.reporting, | |
| 'reportInteraction' | |
| ); | |
| const timeStub = sinon.stub(element.reporting, 'time'); | |
| const timeEndStub = sinon.stub(element.reporting, 'timeEnd'); | |
| element.patchRange = createPatchRange(undefined, 1); | |
| element.latestPatchNum = 1 as PatchSetNumber; | |
| element.editMode = true; | |
| element.path = 'foo.ts'; | |
| element.changeNum = 42 as NumericChangeId; | |
| await element.updateComplete; | |
| const invalidGroup = new GrDiffGroup({ | |
| type: GrDiffGroupType.BOTH, | |
| lines: [new GrDiffLine(GrDiffLineType.BOTH, 1, 1)], | |
| }); | |
| let completed = false; | |
| await element.handleRevertDelta(invalidGroup, () => { | |
| completed = true; | |
| }); | |
| assert.isTrue(completed); | |
| assert.isTrue(reportInteractionStub.calledOnce); | |
| assert.isTrue(timeStub.calledOnce); | |
| assert.isTrue( | |
| timeEndStub.calledWithExactly(Timing.REVERT_DELTA_LOAD, { | |
| success: false, | |
| reason: 'no-fix-suggestion', | |
| }) | |
| ); | |
| }); | |
| test('handleRevertDelta reports REVERT_DELTA_LOAD before navigation resets timers', async () => { | |
| stubRestApi('applyFixSuggestion').returns( | |
| Promise.resolve(new Response(null, {status: 200})) | |
| ); | |
| const reloadStub = sinon.stub(element, 'reload').resolves(); | |
| const setUrlStub = sinon.stub(testResolver(navigationToken), 'setUrl'); | |
| const timeEndStub = sinon.stub(element.reporting, 'timeEnd'); | |
| const onCompleteSpy = sinon.spy(); | |
| element.patchRange = createPatchRange(undefined, 1); | |
| element.latestPatchNum = 1 as PatchSetNumber; | |
| element.editMode = true; | |
| element.path = 'foo.ts'; | |
| element.changeNum = 42 as NumericChangeId; | |
| await element.updateComplete; | |
| const removeLine = new GrDiffLine(GrDiffLineType.REMOVE, 1, 0); | |
| removeLine.text = 'old code'; | |
| const group = new GrDiffGroup({ | |
| type: GrDiffGroupType.DELTA, | |
| lines: [removeLine], | |
| }); | |
| assertIsDefined(element.diffElement); | |
| element.diffElement.groups = [group]; | |
| await element.handleRevertDelta(group, onCompleteSpy); | |
| assert.isTrue( | |
| timeEndStub.calledOnceWithExactly(Timing.REVERT_DELTA_LOAD, { | |
| success: true, | |
| status: 200, | |
| strategy: 'apply-fix', | |
| }) | |
| ); | |
| assert.isTrue(timeEndStub.calledBefore(setUrlStub)); | |
| assert.isTrue(timeEndStub.calledBefore(reloadStub)); | |
| assert.isTrue(reloadStub.calledBefore(onCompleteSpy)); | |
| }); | |
| }); | |
| }); |