blob: 43045f7cec7d93d374f7e919dd739b3211a22c8f [file] [log] [blame]
/**
* @license
* Copyright 2018 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import '../../../test/common-test-setup';
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,
DraftInfo,
EDIT,
ImageInfo,
NumericChangeId,
PARENT,
PatchSetNum,
RevisionPatchSetNum,
UrlEncodedCommentId,
} from '../../../types/common';
import {CoverageType} from '../../../types/types';
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, SinonStubbedMember} 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 {fixture, html, assert} from '@open-wc/testing';
import {testResolver} from '../../../test/common-test-setup';
import {userModelToken, UserModel} 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';
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></gr-diff-host>`);
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;
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('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('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);
assertIsDefined(element.diffElement.diffTable);
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.
assertIsDefined(element.diffElement.diffTable);
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
);
assertIsDefined(element.diffElement.diffTable);
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
);
assertIsDefined(element.diffElement.diffTable);
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
);
assertIsDefined(element.diffElement.diffTable);
const diffTable = element.diffElement.diffTable;
const leftImage = query(diffTable, '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 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 = await fixture(html`<gr-diff-host></gr-diff-host>`);
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 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 = await fixture(html`<gr-diff-host></gr-diff-host>`);
element.changeNum = 123 as NumericChangeId;
element.path = 'file.txt';
element.patchRange = createPatchRange(1, 2);
reportStub = sinon.stub(element.reporting, 'reportInteraction');
await element.updateComplete;
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('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', () => {
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('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(
'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);
});
});
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: ['*'.repeat(501)],
},
],
})
);
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: ['*'.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 () => {
coverageProviderStub = sinon
.stub()
.returns(Promise.resolve(exampleRanges));
element = await fixture(html`<gr-diff-host></gr-diff-host>`);
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;
await element.updateComplete;
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));
});
});
});
});