| /** |
| * @license |
| * Copyright 2018 Google LLC |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| import '../../../test/common-test-setup-karma'; |
| import './gr-diff-host'; |
| import { |
| CommentSide, |
| createDefaultDiffPrefs, |
| Side, |
| } from '../../../constants/constants'; |
| import { |
| createAccountDetailWithId, |
| createBlame, |
| createChange, |
| createComment, |
| createCommentThread, |
| createDiff, |
| createPatchRange, |
| createRunResult, |
| } from '../../../test/test-data-generators'; |
| import { |
| addListenerForTest, |
| mockPromise, |
| query, |
| queryAll, |
| queryAndAssert, |
| stubReporting, |
| stubRestApi, |
| } from '../../../test/test-utils'; |
| import { |
| BasePatchSetNum, |
| BlameInfo, |
| CommentRange, |
| CommitId, |
| EDIT, |
| ImageInfo, |
| NumericChangeId, |
| PARENT, |
| PatchSetNum, |
| RepoName, |
| RevisionPatchSetNum, |
| UrlEncodedCommentId, |
| } from '../../../types/common'; |
| import {CoverageType} from '../../../types/types'; |
| import {GerritNav} from '../../core/gr-navigation/gr-navigation'; |
| import {GrDiffBuilderImage} from '../../../embed/diff/gr-diff-builder/gr-diff-builder-image'; |
| import {GrDiffHost, LineInfo} from './gr-diff-host'; |
| import {DiffInfo, DiffViewMode, IgnoreWhitespaceType} from '../../../api/diff'; |
| import {ErrorCallback} from '../../../api/rest'; |
| import {SinonStub} from 'sinon'; |
| import {RunResult} from '../../../models/checks/checks-model'; |
| import {GrCommentThread} from '../../shared/gr-comment-thread/gr-comment-thread'; |
| import {assertIsDefined} from '../../../utils/common-util'; |
| import {GrAnnotationActionsInterface} from '../../shared/gr-js-api-interface/gr-annotation-actions-js-api'; |
| |
| const basicFixture = fixtureFromElement('gr-diff-host'); |
| |
| suite('gr-diff-host tests', () => { |
| let element: GrDiffHost; |
| let account = createAccountDetailWithId(1); |
| let getDiffRestApiStub: SinonStub; |
| |
| setup(async () => { |
| stubRestApi('getAccount').callsFake(() => Promise.resolve(account)); |
| element = basicFixture.instantiate(); |
| element.changeNum = 123 as NumericChangeId; |
| element.path = 'some/path'; |
| element.change = createChange(); |
| element.patchRange = createPatchRange(); |
| getDiffRestApiStub = stubRestApi('getDiff'); |
| // Fall back in case a test forgets to set one up |
| getDiffRestApiStub.returns(Promise.resolve(createDiff())); |
| await element.updateComplete; |
| }); |
| |
| suite('plugin layers', () => { |
| let getDiffLayersStub: sinon.SinonStub; |
| const pluginLayers = [{annotate: () => {}}, {annotate: () => {}}]; |
| setup(async () => { |
| element = basicFixture.instantiate(); |
| getDiffLayersStub = sinon |
| .stub(element.jsAPI, 'getDiffLayers') |
| .returns(pluginLayers); |
| element.changeNum = 123 as NumericChangeId; |
| element.change = createChange(); |
| element.patchRange = createPatchRange(); |
| element.path = 'some/path'; |
| await element.updateComplete; |
| }); |
| |
| test('plugin layers requested', async () => { |
| getDiffRestApiStub.returns(Promise.resolve(createDiff())); |
| await element.reload(); |
| assert(getDiffLayersStub.called); |
| }); |
| }); |
| |
| 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('reload() cancels before network resolves', async () => { |
| assertIsDefined(element.diffElement); |
| const cancelStub = sinon.stub(element.diffElement, 'cancel'); |
| |
| // Stub the network calls into requests that never resolve. |
| sinon.stub(element, 'getDiff').callsFake(() => new Promise(() => {})); |
| element.patchRange = createPatchRange(); |
| element.change = createChange(); |
| element.prefs = undefined; |
| |
| // Needs to be set to something first for it to cancel. |
| element.diff = createDiff(); |
| await element.updateComplete; |
| |
| element.reload(); |
| assert.isTrue(cancelStub.called); |
| }); |
| |
| test('reload() loads files weblinks', async () => { |
| element.change = createChange(); |
| const weblinksStub = sinon |
| .stub(GerritNav, '_generateWeblinks') |
| .returns({name: 'stubb', url: '#s'}); |
| getDiffRestApiStub.returns(Promise.resolve(createDiff())); |
| element.projectName = 'test-project' as RepoName; |
| element.path = 'test-path'; |
| element.commitRange = { |
| baseCommit: 'test-base' as CommitId, |
| commit: 'test-commit' as CommitId, |
| }; |
| element.patchRange = createPatchRange(); |
| |
| await element.reload(); |
| |
| assert.equal(weblinksStub.callCount, 3); |
| assert.deepEqual(weblinksStub.firstCall.args[0], { |
| commit: 'test-base' as CommitId, |
| file: 'test-path', |
| options: { |
| weblinks: undefined, |
| }, |
| repo: 'test-project' as RepoName, |
| type: GerritNav.WeblinkType.EDIT, |
| }); |
| assert.deepEqual(element.editWeblinks, [ |
| { |
| name: 'stubb', |
| url: '#s', |
| }, |
| ]); |
| assert.deepEqual(weblinksStub.secondCall.args[0], { |
| commit: 'test-base' as CommitId, |
| file: 'test-path', |
| options: { |
| weblinks: undefined, |
| }, |
| repo: 'test-project' as RepoName, |
| type: GerritNav.WeblinkType.FILE, |
| }); |
| assert.deepEqual(weblinksStub.thirdCall.args[0], { |
| commit: 'test-commit' as CommitId, |
| file: 'test-path', |
| options: { |
| weblinks: undefined, |
| }, |
| repo: 'test-project' as RepoName, |
| type: GerritNav.WeblinkType.FILE, |
| }); |
| assert.deepEqual(element.filesWeblinks, { |
| meta_a: [{name: 'stubb', url: '#s'}], |
| meta_b: [{name: 'stubb', url: '#s'}], |
| }); |
| }); |
| |
| 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: ImageInfo; |
| let mockFile2: ImageInfo; |
| 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(); |
| |
| // Recognizes that it should be an image diff. |
| assert.isTrue(element.isImageDiff); |
| assertIsDefined(element.diffElement); |
| assert.instanceOf( |
| element.diffElement.diffBuilder.builder, |
| GrDiffBuilderImage |
| ); |
| |
| // Left image rendered with the parent commit's version of the file. |
| assertIsDefined(element.diffElement); |
| const diffTable = element.diffElement.$.diffTable; |
| const leftImage = queryAndAssert(diffTable, 'td.left img'); |
| const leftLabel = queryAndAssert(diffTable, 'td.left label'); |
| const leftLabelContent = leftLabel.querySelector('.label'); |
| const leftLabelName = leftLabel.querySelector('.name'); |
| |
| const rightImage = queryAndAssert(diffTable, 'td.right img'); |
| const rightLabel = queryAndAssert(diffTable, '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(); |
| |
| // Recognizes that it should be an image diff. |
| assert.isTrue(element.isImageDiff); |
| assertIsDefined(element.diffElement); |
| assert.instanceOf( |
| element.diffElement.diffBuilder.builder, |
| GrDiffBuilderImage |
| ); |
| |
| // Left image rendered with the parent commit's version of the file. |
| const diffTable = element.diffElement.$.diffTable; |
| const leftImage = queryAndAssert(diffTable, 'td.left img'); |
| const leftLabel = queryAndAssert(diffTable, 'td.left label'); |
| const leftLabelContent = leftLabel.querySelector('.label'); |
| const leftLabelName = leftLabel.querySelector('.name'); |
| |
| const rightImage = queryAndAssert(diffTable, 'td.right img'); |
| const rightLabel = queryAndAssert(diffTable, '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(() => { |
| // Recognizes that it should be an image diff. |
| assert.isTrue(element.isImageDiff); |
| assertIsDefined(element.diffElement); |
| assert.instanceOf( |
| element.diffElement.diffBuilder.builder, |
| GrDiffBuilderImage |
| ); |
| |
| const diffTable = element.diffElement.$.diffTable; |
| |
| const leftImage = query(diffTable, 'td.left img'); |
| const rightImage = queryAndAssert(diffTable, '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(() => { |
| // Recognizes that it should be an image diff. |
| assert.isTrue(element.isImageDiff); |
| assertIsDefined(element.diffElement); |
| assert.instanceOf( |
| element.diffElement.diffBuilder.builder, |
| GrDiffBuilderImage |
| ); |
| |
| const diffTable = element.diffElement.$.diffTable; |
| |
| const leftImage = queryAndAssert(diffTable, 'td.left img'); |
| const rightImage = query(diffTable, '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(() => { |
| // Recognizes that it should be an image diff. |
| assert.isTrue(element.isImageDiff); |
| assertIsDefined(element.diffElement); |
| assert.instanceOf( |
| element.diffElement.diffBuilder.builder, |
| GrDiffBuilderImage |
| ); |
| const diffTable = element.diffElement.$.diffTable; |
| |
| const leftImage = query(diffTable, 'td.left img'); |
| assert.isNotOk(leftImage); |
| }); |
| }); |
| }); |
| |
| test('cannot create comments when not logged in', () => { |
| element.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 cancel()', () => { |
| assertIsDefined(element.diffElement); |
| const stub = sinon.stub(element.diffElement, 'cancel'); |
| element.patchRange = createPatchRange(); |
| element.cancel(); |
| assert.isTrue(stub.calledOnce); |
| assert.equal(stub.lastCall.args.length, 0); |
| }); |
| |
| 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('delegates toggleLeftDiff()', () => { |
| assertIsDefined(element.diffElement); |
| const stub = sinon.stub(element.diffElement, 'toggleLeftDiff'); |
| element.toggleLeftDiff(); |
| assert.isTrue(stub.calledOnce); |
| assert.equal(stub.lastCall.args.length, 0); |
| }); |
| |
| suite('blame', () => { |
| setup(async () => { |
| element = basicFixture.instantiate(); |
| element.changeNum = 123 as NumericChangeId; |
| element.path = 'some/path'; |
| await element.updateComplete; |
| }); |
| |
| test('clearBlame', async () => { |
| element.blame = []; |
| await element.updateComplete; |
| assertIsDefined(element.diffElement); |
| const setBlameSpy = sinon.spy( |
| element.diffElement.diffBuilder, |
| 'setBlame' |
| ); |
| const isBlameLoadedStub = sinon.stub(); |
| element.addEventListener('is-blame-loaded-changed', isBlameLoadedStub); |
| element.clearBlame(); |
| await element.updateComplete; |
| assert.isNull(element.blame); |
| assert.isTrue(setBlameSpy.calledWithExactly(null)); |
| 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('getThreadEls() returns .comment-threads', () => { |
| const threadEl = document.createElement('gr-comment-thread'); |
| threadEl.className = 'comment-thread'; |
| assertIsDefined(element.diffElement); |
| element.diffElement.appendChild(threadEl); |
| assert.deepEqual(element.getThreadEls(), [threadEl]); |
| }); |
| |
| test('delegates addDraftAtLine(el)', () => { |
| const param0 = document.createElement('b'); |
| assertIsDefined(element.diffElement); |
| const stub = sinon.stub(element.diffElement, 'addDraftAtLine'); |
| element.addDraftAtLine(param0); |
| assert.isTrue(stub.calledOnce); |
| assert.equal(stub.lastCall.args.length, 1); |
| assert.equal(stub.lastCall.args[0], param0); |
| }); |
| |
| test('delegates clearDiffContent()', () => { |
| assertIsDefined(element.diffElement); |
| const stub = sinon.stub(element.diffElement, 'clearDiffContent'); |
| element.clearDiffContent(); |
| assert.isTrue(stub.calledOnce); |
| assert.equal(stub.lastCall.args.length, 0); |
| }); |
| |
| 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 displayLine', async () => { |
| const value = true; |
| element.displayLine = value; |
| await element.updateComplete; |
| assertIsDefined(element.diffElement); |
| assert.equal(element.diffElement.displayLine, 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: SinonStub; |
| |
| setup(async () => { |
| element = basicFixture.instantiate(); |
| element.changeNum = 123 as NumericChangeId; |
| element.path = 'file.txt'; |
| element.patchRange = createPatchRange(1, 2); |
| reportStub = sinon.stub(element.reporting, 'reportInteraction'); |
| await element.updateComplete; |
| }); |
| |
| 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('createCheckEl method', () => { |
| test('start_line:12', () => { |
| const result: RunResult = { |
| ...createRunResult(), |
| codePointers: [{path: 'a', range: {start_line: 12} as CommentRange}], |
| }; |
| const el = element.createCheckEl(result); |
| assert.equal(el.getAttribute('slot'), 'right-12'); |
| assert.equal(el.getAttribute('diff-side'), 'right'); |
| assert.equal(el.getAttribute('line-num'), '12'); |
| assert.equal(el.getAttribute('range'), null); |
| assert.equal(el.result, result); |
| }); |
| |
| test('start_line:13 end_line:14 without char positions', () => { |
| const result: RunResult = { |
| ...createRunResult(), |
| codePointers: [ |
| {path: 'a', range: {start_line: 13, end_line: 14} as CommentRange}, |
| ], |
| }; |
| const el = element.createCheckEl(result); |
| assert.equal(el.getAttribute('slot'), 'right-14'); |
| assert.equal(el.getAttribute('diff-side'), 'right'); |
| assert.equal(el.getAttribute('line-num'), '14'); |
| assert.equal(el.getAttribute('range'), null); |
| assert.equal(el.result, result); |
| }); |
| |
| test('start_line:13 end_line:14 with char positions', () => { |
| const result: RunResult = { |
| ...createRunResult(), |
| codePointers: [ |
| { |
| path: 'a', |
| range: { |
| start_line: 13, |
| end_line: 14, |
| start_character: 5, |
| end_character: 7, |
| }, |
| }, |
| ], |
| }; |
| const el = element.createCheckEl(result); |
| assert.equal(el.getAttribute('slot'), 'right-14'); |
| assert.equal(el.getAttribute('diff-side'), 'right'); |
| assert.equal(el.getAttribute('line-num'), '14'); |
| assert.equal( |
| el.getAttribute('range'), |
| '{"start_line":13,' + |
| '"end_line":14,' + |
| '"start_character":5,' + |
| '"end_character":7}' |
| ); |
| assert.equal(el.result, result); |
| }); |
| |
| test('empty range', () => { |
| const result: RunResult = { |
| ...createRunResult(), |
| codePointers: [{path: 'a', range: {} as CommentRange}], |
| }; |
| const el = element.createCheckEl(result); |
| assert.equal(el.getAttribute('slot'), 'right-FILE'); |
| assert.equal(el.getAttribute('diff-side'), 'right'); |
| assert.equal(el.getAttribute('line-num'), 'FILE'); |
| assert.equal(el.getAttribute('range'), null); |
| assert.equal(el.result, result); |
| }); |
| }); |
| |
| suite('create-comment', () => { |
| setup(async () => { |
| 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', |
| }, |
| }) |
| ); |
| assertIsDefined(element.diffElement); |
| let threads = |
| element.diffElement.querySelectorAll<GrCommentThread>( |
| 'gr-comment-thread' |
| ); |
| |
| assert.equal(threads.length, 1); |
| assert.equal(threads[0].thread?.commentSide, CommentSide.PARENT); |
| assert.equal(threads[0].getAttribute('diff-side'), Side.LEFT); |
| assert.equal(threads[0].thread?.range, undefined); |
| assert.equal(threads[0].thread?.patchNum, 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, |
| }, |
| }) |
| ); |
| |
| assertIsDefined(element.diffElement); |
| threads = |
| element.diffElement.querySelectorAll<GrCommentThread>( |
| 'gr-comment-thread' |
| ); |
| |
| assert.equal(threads.length, 2); |
| assert.equal(threads[0].thread?.commentSide, CommentSide.PARENT); |
| assert.equal(threads[0].getAttribute('diff-side'), Side.LEFT); |
| assert.equal(threads[1].thread?.range, range); |
| assert.equal(threads[1].thread?.patchNum, 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, |
| }, |
| }) |
| ); |
| |
| assertIsDefined(element.diffElement); |
| const threads = |
| element.diffElement.querySelectorAll<GrCommentThread>( |
| 'gr-comment-thread' |
| ); |
| assert.equal(threads.length, 1); |
| const threadEl = threads[0]; |
| |
| assert.equal(threadEl.thread?.commentSide, CommentSide.REVISION); |
| assert.equal(threadEl.getAttribute('diff-side'), Side.RIGHT); |
| }); |
| |
| test('should be on parent if right and base is PARENT', () => { |
| element.patchRange = createPatchRange(); |
| |
| element.dispatchEvent( |
| new CustomEvent('create-comment', { |
| detail: { |
| side: Side.LEFT, |
| }, |
| }) |
| ); |
| |
| assertIsDefined(element.diffElement); |
| const threads = |
| element.diffElement.querySelectorAll<GrCommentThread>( |
| 'gr-comment-thread' |
| ); |
| const threadEl = threads[0]; |
| |
| assert.equal(threadEl.thread?.commentSide, CommentSide.PARENT); |
| assert.equal(threadEl.getAttribute('diff-side'), Side.LEFT); |
| }); |
| |
| 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, |
| }, |
| }) |
| ); |
| |
| assertIsDefined(element.diffElement); |
| const threads = |
| element.diffElement.querySelectorAll<GrCommentThread>( |
| 'gr-comment-thread' |
| ); |
| const threadEl = threads[0]; |
| |
| assert.equal(threadEl.thread?.commentSide, CommentSide.PARENT); |
| assert.equal(threadEl.getAttribute('diff-side'), Side.LEFT); |
| }); |
| |
| test('should not be on parent otherwise', () => { |
| element.patchRange = createPatchRange(2, 3); |
| element.dispatchEvent( |
| new CustomEvent('create-comment', { |
| detail: { |
| side: Side.LEFT, |
| }, |
| }) |
| ); |
| |
| assertIsDefined(element.diffElement); |
| const threads = |
| element.diffElement.querySelectorAll<GrCommentThread>( |
| 'gr-comment-thread' |
| ); |
| const threadEl = threads[0]; |
| |
| assert.equal(threadEl.thread?.commentSide, CommentSide.REVISION); |
| assert.equal(threadEl.getAttribute('diff-side'), Side.LEFT); |
| }); |
| |
| 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', |
| }, |
| }) |
| ); |
| |
| assertIsDefined(element.diffElement); |
| const threads = |
| element.diffElement.querySelectorAll<GrCommentThread>( |
| 'gr-comment-thread' |
| ); |
| assert.equal(threads.length, 1); |
| assert.equal(threads[0].getAttribute('diff-side'), Side.LEFT); |
| assert.equal(threads[0].thread?.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', |
| }, |
| }) |
| ); |
| |
| assertIsDefined(element.diffElement); |
| const threads = |
| element.diffElement.querySelectorAll<GrCommentThread>( |
| 'gr-comment-thread' |
| ); |
| |
| assert.equal(threads.length, 1); |
| assert.equal(threads[0].getAttribute('diff-side'), Side.RIGHT); |
| assert.equal(threads[0].thread?.path, element.file.path); |
| } |
| ); |
| |
| test('multiple threads created on the same range', async () => { |
| element.patchRange = createPatchRange(2, 3); |
| element.file = {basePath: 'file_renamed.txt', path: element.path ?? ''}; |
| await element.updateComplete; |
| |
| const comment = { |
| ...createComment(), |
| range: { |
| start_line: 1, |
| start_character: 1, |
| end_line: 2, |
| end_character: 2, |
| }, |
| patch_set: 3 as RevisionPatchSetNum, |
| }; |
| const thread = createCommentThread([comment]); |
| element.threads = [thread]; |
| await element.updateComplete; |
| |
| assertIsDefined(element.diffElement); |
| let threads = |
| element.diffElement.querySelectorAll<GrCommentThread>( |
| 'gr-comment-thread' |
| ); |
| |
| assert.equal(threads.length, 1); |
| element.threads = [...element.threads, thread]; |
| await element.updateComplete; |
| |
| assertIsDefined(element.diffElement); |
| threads = |
| element.diffElement.querySelectorAll<GrCommentThread>( |
| 'gr-comment-thread' |
| ); |
| // Threads have same rootId so element is reused |
| assert.equal(threads.length, 1); |
| |
| const newThread = {...thread}; |
| newThread.rootId = 'differentRootId' as UrlEncodedCommentId; |
| element.threads = [...element.threads, newThread]; |
| await element.updateComplete; |
| threads = |
| element.diffElement.querySelectorAll<GrCommentThread>( |
| 'gr-comment-thread' |
| ); |
| // New thread has a different rootId |
| assert.equal(threads.length, 2); |
| }); |
| |
| test('unsaved thread changes to draft', async () => { |
| element.patchRange = createPatchRange(2, 3); |
| element.file = {basePath: 'file_renamed.txt', path: element.path ?? ''}; |
| element.threads = []; |
| await element.updateComplete; |
| |
| element.dispatchEvent( |
| new CustomEvent('create-comment', { |
| detail: { |
| side: Side.RIGHT, |
| path: element.path, |
| lineNum: 13, |
| }, |
| }) |
| ); |
| await element.updateComplete; |
| assert.equal(element.getThreadEls().length, 1); |
| const threadEl = element.getThreadEls()[0]; |
| assert.equal(threadEl.thread?.line, 13); |
| assert.isDefined(threadEl.unsavedComment); |
| assert.equal(threadEl.thread?.comments.length, 0); |
| |
| const draftThread = createCommentThread([ |
| { |
| path: element.path, |
| patch_set: 3 as RevisionPatchSetNum, |
| line: 13, |
| __draft: true, |
| }, |
| ]); |
| element.threads = [draftThread]; |
| await element.updateComplete; |
| |
| // We expect that no additional thread element was created. |
| assert.equal(element.getThreadEls().length, 1); |
| // In fact the thread element must still be the same. |
| assert.equal(element.getThreadEls()[0], threadEl); |
| // But it must have been updated from unsaved to draft: |
| assert.isUndefined(threadEl.unsavedComment); |
| assert.equal(threadEl.thread?.comments.length, 1); |
| }); |
| |
| 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', |
| }, |
| }) |
| ); |
| |
| assertIsDefined(element.diffElement); |
| const threads = |
| element.diffElement.querySelectorAll<GrCommentThread>( |
| 'gr-comment-thread' |
| ); |
| |
| assert.equal(threads.length, 1); |
| assert.equal(threads[0].getAttribute('diff-side'), Side.LEFT); |
| assert.equal(threads[0].thread?.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', |
| }, |
| }) |
| ); |
| |
| assertIsDefined(element.diffElement); |
| const threads = |
| element.diffElement.querySelectorAll<GrCommentThread>( |
| 'gr-comment-thread' |
| ); |
| |
| assert.equal(threads.length, 0); |
| 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', |
| }, |
| }) |
| ); |
| |
| assertIsDefined(element.diffElement); |
| const threads = |
| element.diffElement.querySelectorAll<GrCommentThread>( |
| 'gr-comment-thread' |
| ); |
| assert.equal(threads.length, 0); |
| assert.isTrue(alertSpy.called); |
| }); |
| }); |
| |
| test('filterThreadElsForLocation with no threads', () => { |
| const line = {beforeNumber: 3, afterNumber: 5}; |
| const threads: GrCommentThread[] = []; |
| assert.deepEqual( |
| element.filterThreadElsForLocation(threads, line, Side.LEFT), |
| [] |
| ); |
| assert.deepEqual( |
| element.filterThreadElsForLocation(threads, line, Side.RIGHT), |
| [] |
| ); |
| }); |
| |
| test('filterThreadElsForLocation for line comments', () => { |
| const line = {beforeNumber: 3, afterNumber: 5}; |
| |
| const l3 = document.createElement('gr-comment-thread'); |
| l3.setAttribute('line-num', '3'); |
| l3.setAttribute('diff-side', Side.LEFT); |
| |
| const l5 = document.createElement('gr-comment-thread'); |
| l5.setAttribute('line-num', '5'); |
| l5.setAttribute('diff-side', Side.LEFT); |
| |
| const r3 = document.createElement('gr-comment-thread'); |
| r3.setAttribute('line-num', '3'); |
| r3.setAttribute('diff-side', Side.RIGHT); |
| |
| const r5 = document.createElement('gr-comment-thread'); |
| r5.setAttribute('line-num', '5'); |
| r5.setAttribute('diff-side', Side.RIGHT); |
| |
| const threadEls: GrCommentThread[] = [l3, l5, r3, r5]; |
| assert.deepEqual( |
| element.filterThreadElsForLocation(threadEls, line, Side.LEFT), |
| [l3] |
| ); |
| assert.deepEqual( |
| element.filterThreadElsForLocation(threadEls, line, Side.RIGHT), |
| [r5] |
| ); |
| }); |
| |
| test('filterThreadElsForLocation for file comments', () => { |
| const line: LineInfo = {beforeNumber: 'FILE', afterNumber: 'FILE'}; |
| |
| const l = document.createElement('gr-comment-thread'); |
| l.setAttribute('diff-side', Side.LEFT); |
| l.setAttribute('line-num', 'FILE'); |
| |
| const r = document.createElement('gr-comment-thread'); |
| r.setAttribute('diff-side', Side.RIGHT); |
| r.setAttribute('line-num', 'FILE'); |
| |
| const threadEls: GrCommentThread[] = [l, r]; |
| assert.deepEqual( |
| element.filterThreadElsForLocation(threadEls, line, Side.LEFT), |
| [l] |
| ); |
| assert.deepEqual( |
| element.filterThreadElsForLocation(threadEls, line, Side.RIGHT), |
| [r] |
| ); |
| }); |
| |
| 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('rendering large diff disables syntax', async () => { |
| // Before it renders, set the first diff line to 500 '*' characters. |
| getDiffRestApiStub.returns( |
| Promise.resolve({ |
| ...createDiff(), |
| content: [ |
| { |
| a: [new Array(501).join('*')], |
| }, |
| ], |
| }) |
| ); |
| element.reload(); |
| await element.waitForReloadToRender(); |
| assert.isFalse(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: [new Array(501).join('*')], |
| }, |
| ], |
| }) |
| ); |
| await element.waitForReloadToRender(); |
| assert.isFalse(element.syntaxLayer.enabled); |
| }); |
| }); |
| |
| suite('coverage layer', () => { |
| let notifyStub: SinonStub; |
| let coverageProviderStub: SinonStub; |
| let getCoverageAnnotationApisStub: 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 () => { |
| notifyStub = sinon.stub(); |
| coverageProviderStub = sinon |
| .stub() |
| .returns(Promise.resolve(exampleRanges)); |
| |
| element = basicFixture.instantiate(); |
| |
| element.changeNum = 123 as NumericChangeId; |
| element.change = createChange(); |
| element.path = 'some/path'; |
| const prefs = { |
| ...createDefaultDiffPrefs(), |
| line_length: 10, |
| show_tabs: true, |
| tab_size: 4, |
| context: -1, |
| }; |
| element.patchRange = createPatchRange(); |
| element.prefs = prefs; |
| getDiffRestApiStub.returns( |
| Promise.resolve({ |
| ...createDiff(), |
| content: [{a: ['foo']}], |
| }) |
| ); |
| getCoverageAnnotationApisStub = sinon |
| .stub(element.jsAPI, 'getCoverageAnnotationApis') |
| .returns( |
| Promise.resolve([ |
| { |
| notify: notifyStub, |
| getCoverageProvider() { |
| return coverageProviderStub; |
| }, |
| } as unknown as GrAnnotationActionsInterface, |
| ]) |
| ); |
| }); |
| |
| test('getCoverageAnnotationApis should be called', async () => { |
| await element.waitForReloadToRender(); |
| assert.isTrue(getCoverageAnnotationApisStub.calledOnce); |
| }); |
| |
| test('coverageRangeChanged should be called', async () => { |
| await element.waitForReloadToRender(); |
| assert.equal(notifyStub.callCount, 2); |
| assert.isTrue( |
| notifyStub.calledWithExactly('some/path', 1, 2, Side.RIGHT) |
| ); |
| assert.isTrue( |
| notifyStub.calledWithExactly('some/path', 3, 4, Side.RIGHT) |
| ); |
| }); |
| |
| test('provider is called with appropriate params', async () => { |
| element.patchRange = createPatchRange(1, 3); |
| 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)); |
| }); |
| }); |
| }); |
| }); |