Move all server calls to gr-diff-host

This way gr-diff can be reused with a different server.

I decided to move tests concerning the server calls almost unchanged to
gr-diff-host, even when they reach into gr-diff and deeper to check if
certain things happened as a result of loading.

I added similar tests, minus the actual loading, back to gr-diff. This
results in a bit of duplicated test logic, because e.g. the rendering
is tested both in gr-diff and gr-diff-host. I think that is worth it to
reduce the likelihood I broke something.

Bug: Issue 9623
Change-Id: Ib460094eb8d5b8e856c0d7954ee801fa3b5946d3
This commit is contained in:
Ole Rehmsen
2018-08-27 22:43:38 +02:00
parent ffb320ae65
commit b1a7967be2
8 changed files with 963 additions and 733 deletions

View File

@@ -1351,7 +1351,6 @@ limitations under the License.
const setupDiff = function(diff) {
const mock = document.createElement('mock-diff-response');
diff.$.diff._diff = mock.diffResponse;
diff.comments = {
left: diff.path === '/COMMIT_MSG' ? commitMsgComments : [],
right: [],
@@ -1379,7 +1378,7 @@ limitations under the License.
theme: 'DEFAULT',
ignore_whitespace: 'IGNORE_NONE',
};
diff.$.diff._renderDiffTable();
diff._diff = mock.diffResponse;
};
const renderAndGetNewDiffs = function(index) {

View File

@@ -35,6 +35,7 @@ limitations under the License.
<mock-diff-response></mock-diff-response>
<gr-diff></gr-diff>
<gr-diff-cursor></gr-diff-cursor>
<gr-rest-api-interface></gr-rest-api-interface>
</template>
</test-fixture>
@@ -48,27 +49,18 @@ limitations under the License.
setup(done => {
sandbox = sinon.sandbox.create();
stub('gr-rest-api-interface', {
getLoggedIn() { return Promise.resolve(false); },
getDiff() {
return Promise.resolve(mockDiffResponse.diffResponse);
},
});
const fixtureElems = fixture('basic');
mockDiffResponse = fixtureElems[0];
diffElement = fixtureElems[1];
cursorElement = fixtureElems[2];
const restAPI = fixtureElems[3];
// Register the diff with the cursor.
cursorElement.push('diffs', diffElement);
diffElement.loggedIn = false;
diffElement.patchRange = {basePatchNum: 1, patchNum: 2};
diffElement.comments = {left: [], right: []};
diffElement.$.restAPI.getDiffPreferences().then(prefs => {
diffElement.prefs = prefs;
});
const setupDone = () => {
cursorElement._updateStops();
cursorElement.moveToFirstChunk();
@@ -77,7 +69,10 @@ limitations under the License.
};
diffElement.addEventListener('render', setupDone);
diffElement.reload();
restAPI.getDiffPreferences().then(prefs => {
diffElement.prefs = prefs;
diffElement.diff = mockDiffResponse.diffResponse;
});
});
teardown(() => sandbox.restore());
@@ -219,7 +214,7 @@ limitations under the License.
done();
}
diffElement.addEventListener('render', renderHandler);
diffElement.reload();
diffElement._diffChanged(mockDiffResponse.diffResponse);
});
test('initialLineNumber enabled', done => {
@@ -239,7 +234,7 @@ limitations under the License.
cursorElement.initialLineNumber = 10;
cursorElement.side = 'right';
diffElement.reload();
diffElement._diffChanged(mockDiffResponse.diffResponse);
});
test('getAddress', () => {

View File

@@ -16,6 +16,9 @@ limitations under the License.
-->
<link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
<link rel="import" href="../gr-diff/gr-diff.html">
<dom-module id="gr-diff-host">
@@ -30,17 +33,23 @@ limitations under the License.
project-config="[[projectConfig]]"
project-name="[[projectName]]"
display-line="[[displayLine]]"
is-image-diff="{{isImageDiff}}"
is-image-diff="[[isImageDiff]]"
commit-range="[[commitRange]]"
files-weblinks="{{filesWeblinks}}"
hidden$="[[hidden]]"
no-render-on-prefs-change="[[noRenderOnPrefsChange]]"
comments="[[comments]]"
line-wrapping="[[lineWrapping]]"
view-mode="[[viewMode]]"
line-of-interest="[[lineOfInterest]]"
show-load-failure="[[showLoadFailure]]"
is-blame-loaded="{{isBlameLoaded}}"></gr-diff>
logged-in="[[_loggedIn]]"
loading="[[_loading]]"
error-message="[[_errorMessage]]"
base-image="[[_baseImage]]"
revision-image=[[_revisionImage]]
blame="[[_blame]]"
diff="[[_diff]]"></gr-diff>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
<gr-reporting id="reporting" category="diff"></gr-reporting>
</template>
<script src="gr-diff-host.js"></script>
</dom-module>

View File

@@ -17,11 +17,32 @@
(function() {
'use strict';
const MSG_EMPTY_BLAME = 'No blame information for this diff.';
const EVENT_AGAINST_PARENT = 'diff-against-parent';
const EVENT_ZERO_REBASE = 'rebase-percent-zero';
const EVENT_NONZERO_REBASE = 'rebase-percent-nonzero';
const DiffViewMode = {
SIDE_BY_SIDE: 'SIDE_BY_SIDE',
UNIFIED: 'UNIFIED_DIFF',
};
/**
* @param {Object} diff
* @return {boolean}
*/
function isImageDiff(diff) {
if (!diff) { return false; }
const isA = diff.meta_a &&
diff.meta_a.content_type.startsWith('image/');
const isB = diff.meta_b &&
diff.meta_b.content_type.startsWith('image/');
return !!(diff.binary && (isA || isB));
}
/**
* Wrapper around gr-diff.
*
@@ -71,11 +92,13 @@
},
isImageDiff: {
type: Boolean,
computed: '_computeIsImageDiff(_diff)',
notify: true,
},
commitRange: Object,
filesWeblinks: {
type: Object,
value() { return {}; },
notify: true,
},
hidden: {
@@ -113,12 +136,109 @@
isBlameLoaded: {
type: Boolean,
notify: true,
computed: '_computeIsBlameLoaded(_blame)',
},
_loggedIn: {
type: Boolean,
value: false,
},
_loading: {
type: Boolean,
value: false,
},
/** @type {?string} */
_errorMessage: {
type: String,
value: null,
},
/** @type {?Object} */
_baseImage: Object,
/** @type {?Object} */
_revisionImage: Object,
_diff: Object,
/** @type {?Object} */
_blame: {
type: Object,
value: null,
},
},
listeners: {
'draft-interaction': '_handleDraftInteraction',
},
ready() {
if (this._canReload()) {
this.reload();
}
},
attached() {
this._getLoggedIn().then(loggedIn => {
this._loggedIn = loggedIn;
});
},
/** @return {!Promise} */
reload() {
return this.$.diff.reload();
this._loading = true;
this._errorMessage = null;
const diffRequest = this._getDiff()
.then(diff => {
this._reportDiff(diff);
return diff;
})
.catch(e => {
this._handleGetDiffError(e);
return null;
});
const assetRequest = diffRequest.then(diff => {
// If the diff is null, then it's failed to load.
if (!diff) { return null; }
return this._loadDiffAssets(diff);
});
return Promise.all([diffRequest, assetRequest])
.then(results => {
const diff = results[0];
if (!diff) {
return Promise.resolve();
}
this.filesWeblinks = this._getFilesWeblinks(diff);
return new Promise(resolve => {
const callback = () => {
resolve();
this.removeEventListener('render', callback);
};
this.addEventListener('render', callback);
this._diff = diff;
});
})
.catch(err => {
console.warn('Error encountered loading diff:', err);
})
.then(() => { this._loading = false; });
},
_getFilesWeblinks(diff) {
if (!this.commitRange) { return {}; }
return {
meta_a: Gerrit.Nav.getFileWebLinks(
this.projectName, this.commitRange.baseCommit, this.path,
{weblinks: diff && diff.meta_a && diff.meta_a.web_links}),
meta_b: Gerrit.Nav.getFileWebLinks(
this.projectName, this.commitRange.commit, this.path,
{weblinks: diff && diff.meta_b && diff.meta_b.web_links}),
};
},
/** Cancel any remaining diff builder rendering work. */
@@ -145,12 +265,21 @@
* @return {Promise} A promise that resolves when blame finishes rendering.
*/
loadBlame() {
return this.$.diff.loadBlame();
return this.$.restAPI.getBlame(this.changeNum, this.patchRange.patchNum,
this.path, true)
.then(blame => {
if (!blame.length) {
this.fire('show-alert', {message: MSG_EMPTY_BLAME});
return Promise.reject(MSG_EMPTY_BLAME);
}
this._blame = blame;
});
},
/** Unload blame information for the diff. */
clearBlame() {
this.$.diff.clearBlame();
this._blame = null;
},
/** @return {!Array<!HTMLElement>} */
@@ -170,5 +299,136 @@
expandAllContext() {
this.$.diff.expandAllContext();
},
/** @return {!Promise} */
_getLoggedIn() {
return this.$.restAPI.getLoggedIn();
},
/** @return {boolean}} */
_canReload() {
return !!this.changeNum && !!this.patchRange && !!this.path &&
!this.noAutoRender;
},
/** @return {!Promise<!Object>} */
_getDiff() {
// Wrap the diff request in a new promise so that the error handler
// rejects the promise, allowing the error to be handled in the .catch.
return new Promise((resolve, reject) => {
this.$.restAPI.getDiff(
this.changeNum,
this.patchRange.basePatchNum,
this.patchRange.patchNum,
this.path,
reject)
.then(resolve);
});
},
_handleGetDiffError(response) {
// Loading the diff may respond with 409 if the file is too large. In this
// case, use a toast error..
if (response.status === 409) {
this.fire('server-error', {response});
return;
}
if (this.showLoadFailure) {
this._errorMessage = [
'Encountered error when loading the diff:',
response.status,
response.statusText,
].join(' ');
return;
}
this.fire('page-error', {response});
},
/**
* Report info about the diff response.
*/
_reportDiff(diff) {
if (!diff || !diff.content) { return; }
// Count the delta lines stemming from normal deltas, and from
// due_to_rebase deltas.
let nonRebaseDelta = 0;
let rebaseDelta = 0;
diff.content.forEach(chunk => {
if (chunk.ab) { return; }
const deltaSize = Math.max(
chunk.a ? chunk.a.length : 0, chunk.b ? chunk.b.length : 0);
if (chunk.due_to_rebase) {
rebaseDelta += deltaSize;
} else {
nonRebaseDelta += deltaSize;
}
});
// Find the percent of the delta from due_to_rebase chunks rounded to two
// digits. Diffs with no delta are considered 0%.
const totalDelta = rebaseDelta + nonRebaseDelta;
const percentRebaseDelta = !totalDelta ? 0 :
Math.round(100 * rebaseDelta / totalDelta);
// Report the due_to_rebase percentage in the "diff" category when
// applicable.
if (this.patchRange.basePatchNum === 'PARENT') {
this.$.reporting.reportInteraction(EVENT_AGAINST_PARENT);
} else if (percentRebaseDelta === 0) {
this.$.reporting.reportInteraction(EVENT_ZERO_REBASE);
} else {
this.$.reporting.reportInteraction(EVENT_NONZERO_REBASE,
percentRebaseDelta);
}
},
/**
* @param {Object} diff
* @return {!Promise}
*/
_loadDiffAssets(diff) {
if (isImageDiff(diff)) {
return this._getImages(diff).then(images => {
this._baseImage = images.baseImage;
this._revisionImage = images.revisionImage;
});
} else {
this._baseImage = null;
this._revisionImage = null;
return Promise.resolve();
}
},
/**
* @param {Object} diff
* @return {boolean}
*/
_computeIsImageDiff(diff) {
return isImageDiff(diff);
},
/**
* @param {Object} blame
* @return {boolean}
*/
_computeIsBlameLoaded(blame) {
return !!blame;
},
/**
* @param {Object} diff
* @return {!Promise}
*/
_getImages(diff) {
return this.$.restAPI.getImagesForDiff(this.changeNum, diff,
this.patchRange);
},
_handleDraftInteraction() {
this.$.reporting.recordDraftInteraction();
},
});
})();

View File

@@ -40,23 +40,437 @@ limitations under the License.
setup(() => {
sandbox = sinon.sandbox.create();
element = fixture('basic');
});
teardown(() => {
sandbox.restore();
});
test('delegates reload()', () => {
element = fixture('basic');
const returnValue = Promise.resolve();
const stub = sandbox.stub(element.$.diff, 'reload').returns(returnValue);
assert.equal(element.reload(), returnValue);
assert.isTrue(stub.calledOnce);
assert.equal(stub.lastCall.args.length, 0);
test('reload() cancels before network resolves', () => {
const cancelStub = sandbox.stub(element.$.diff, 'cancel');
// Stub the network calls into requests that never resolve.
sandbox.stub(element, '_getDiff', () => new Promise(() => {}));
element.reload();
assert.isTrue(cancelStub.called);
});
suite('not logged in', () => {
setup(() => {
const getLoggedInPromise = Promise.resolve(false);
stub('gr-rest-api-interface', {
getLoggedIn() { return getLoggedInPromise; },
});
element = fixture('basic');
return getLoggedInPromise;
});
test('reload() loads files weblinks', () => {
const weblinksStub = sandbox.stub(Gerrit.Nav, '_generateWeblinks')
.returns({name: 'stubb', url: '#s'});
sandbox.stub(element.$.restAPI, 'getDiff').returns(Promise.resolve({
content: [],
}));
element.projectName = 'test-project';
element.path = 'test-path';
element.commitRange = {baseCommit: 'test-base', commit: 'test-commit'};
element.patchRange = {};
return element.reload().then(() => {
assert.isTrue(weblinksStub.calledTwice);
assert.isTrue(weblinksStub.firstCall.calledWith({
commit: 'test-base',
file: 'test-path',
options: {
weblinks: undefined,
},
repo: 'test-project',
type: Gerrit.Nav.WeblinkType.FILE}));
assert.isTrue(weblinksStub.secondCall.calledWith({
commit: 'test-commit',
file: 'test-path',
options: {
weblinks: undefined,
},
repo: 'test-project',
type: Gerrit.Nav.WeblinkType.FILE}));
assert.deepEqual(element.filesWeblinks, {
meta_a: [{name: 'stubb', url: '#s'}],
meta_b: [{name: 'stubb', url: '#s'}],
});
});
});
test('_getDiff handles null diff responses', done => {
stub('gr-rest-api-interface', {
getDiff() { return Promise.resolve(null); },
});
element.changeNum = 123;
element.patchRange = {basePatchNum: 1, patchNum: 2};
element.path = 'file.txt';
element._getDiff().then(done);
});
test('reload resolves on error', () => {
const onErrStub = sandbox.stub(element, '_handleGetDiffError');
const error = {ok: false, status: 500};
sandbox.stub(element.$.restAPI, 'getDiff',
(changeNum, basePatchNum, patchNum, path, onErr) => {
onErr(error);
});
return element.reload().then(() => {
assert.isTrue(onErrStub.calledOnce);
});
});
suite('_handleGetDiffError', () => {
let serverErrorStub;
let pageErrorStub;
setup(() => {
serverErrorStub = sinon.stub();
element.addEventListener('server-error', serverErrorStub);
pageErrorStub = sinon.stub();
element.addEventListener('page-error', pageErrorStub);
});
test('page error on HTTP-409', () => {
element._handleGetDiffError({status: 409});
assert.isTrue(serverErrorStub.calledOnce);
assert.isFalse(pageErrorStub.called);
assert.isNotOk(element._errorMessage);
});
test('server error on non-HTTP-409', () => {
element._handleGetDiffError({status: 500});
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!'});
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;
let mockFile2;
setup(() => {
mockFile1 = {
body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' +
'wsAAAAAAAAAAAAAAAAA/w==',
type: 'image/bmp',
};
mockFile2 = {
body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' +
'wsAAAAAAAAAAAAA/////w==',
type: 'image/bmp',
};
sandbox.stub(element.$.restAPI,
'getB64FileContents',
(changeId, patchNum, path, opt_parentIndex) => {
return Promise.resolve(opt_parentIndex === 1 ? mockFile1 :
mockFile2);
});
element.patchRange = {basePatchNum: 'PARENT', patchNum: 1};
element.comments = {left: [], right: []};
});
test('renders image diffs with same file name', done => {
const mockDiff = {
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,
};
sandbox.stub(element.$.restAPI, 'getDiff')
.returns(Promise.resolve(mockDiff));
const rendered = () => {
// Recognizes that it should be an image diff.
assert.isTrue(element.isImageDiff);
assert.instanceOf(
element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage);
// Left image rendered with the parent commit's version of the file.
const leftImage =
element.$.diff.$.diffTable.querySelector('td.left img');
const leftLabel =
element.$.diff.$.diffTable.querySelector('td.left label');
const leftLabelContent = leftLabel.querySelector('.label');
const leftLabelName = leftLabel.querySelector('.name');
const rightImage =
element.$.diff.$.diffTable.querySelector('td.right img');
const rightLabel = element.$.diff.$.diffTable.querySelector(
'td.right label');
const rightLabelContent = rightLabel.querySelector('.label');
const rightLabelName = rightLabel.querySelector('.name');
assert.isNotOk(rightLabelName);
assert.isNotOk(leftLabelName);
let leftLoaded = false;
let rightLoaded = false;
leftImage.addEventListener('load', () => {
assert.isOk(leftImage);
assert.equal(leftImage.getAttribute('src'),
'data:image/bmp;base64, ' + mockFile1.body);
assert.equal(leftLabelContent.textContent, '1×1 image/bmp');
leftLoaded = true;
if (rightLoaded) {
element.removeEventListener('render', rendered);
done();
}
});
rightImage.addEventListener('load', () => {
assert.isOk(rightImage);
assert.equal(rightImage.getAttribute('src'),
'data:image/bmp;base64, ' + mockFile2.body);
assert.equal(rightLabelContent.textContent, '1×1 image/bmp');
rightLoaded = true;
if (leftLoaded) {
element.removeEventListener('render', rendered);
done();
}
});
};
element.addEventListener('render', rendered);
element.$.restAPI.getDiffPreferences().then(prefs => {
element.prefs = prefs;
element.reload();
});
});
test('renders image diffs with a different file name', done => {
const mockDiff = {
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,
};
sandbox.stub(element.$.restAPI, 'getDiff')
.returns(Promise.resolve(mockDiff));
const rendered = () => {
// Recognizes that it should be an image diff.
assert.isTrue(element.isImageDiff);
assert.instanceOf(
element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage);
// Left image rendered with the parent commit's version of the file.
const leftImage =
element.$.diff.$.diffTable.querySelector('td.left img');
const leftLabel =
element.$.diff.$.diffTable.querySelector('td.left label');
const leftLabelContent = leftLabel.querySelector('.label');
const leftLabelName = leftLabel.querySelector('.name');
const rightImage =
element.$.diff.$.diffTable.querySelector('td.right img');
const rightLabel = element.$.diff.$.diffTable.querySelector(
'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);
let leftLoaded = false;
let rightLoaded = false;
leftImage.addEventListener('load', () => {
assert.isOk(leftImage);
assert.equal(leftImage.getAttribute('src'),
'data:image/bmp;base64, ' + mockFile1.body);
assert.equal(leftLabelContent.textContent, '1×1 image/bmp');
leftLoaded = true;
if (rightLoaded) {
element.removeEventListener('render', rendered);
done();
}
});
rightImage.addEventListener('load', () => {
assert.isOk(rightImage);
assert.equal(rightImage.getAttribute('src'),
'data:image/bmp;base64, ' + mockFile2.body);
assert.equal(rightLabelContent.textContent, '1×1 image/bmp');
rightLoaded = true;
if (leftLoaded) {
element.removeEventListener('render', rendered);
done();
}
});
};
element.addEventListener('render', rendered);
element.$.restAPI.getDiffPreferences().then(prefs => {
element.prefs = prefs;
element.reload();
});
});
test('renders added image', done => {
const mockDiff = {
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,
};
sandbox.stub(element.$.restAPI, 'getDiff')
.returns(Promise.resolve(mockDiff));
element.addEventListener('render', () => {
// Recognizes that it should be an image diff.
assert.isTrue(element.isImageDiff);
assert.instanceOf(
element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage);
const leftImage =
element.$.diff.$.diffTable.querySelector('td.left img');
const rightImage =
element.$.diff.$.diffTable.querySelector('td.right img');
assert.isNotOk(leftImage);
assert.isOk(rightImage);
done();
});
element.$.restAPI.getDiffPreferences().then(prefs => {
element.prefs = prefs;
element.reload();
});
});
test('renders removed image', done => {
const mockDiff = {
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,
};
sandbox.stub(element.$.restAPI, 'getDiff')
.returns(Promise.resolve(mockDiff));
element.addEventListener('render', () => {
// Recognizes that it should be an image diff.
assert.isTrue(element.isImageDiff);
assert.instanceOf(
element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage);
const leftImage =
element.$.diff.$.diffTable.querySelector('td.left img');
const rightImage =
element.$.diff.$.diffTable.querySelector('td.right img');
assert.isOk(leftImage);
assert.isNotOk(rightImage);
done();
});
element.$.restAPI.getDiffPreferences().then(prefs => {
element.prefs = prefs;
element.reload();
});
});
test('does not render disallowed image type', done => {
const mockDiff = {
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';
sandbox.stub(element.$.restAPI, 'getDiff')
.returns(Promise.resolve(mockDiff));
element.addEventListener('render', () => {
// Recognizes that it should be an image diff.
assert.isTrue(element.isImageDiff);
assert.instanceOf(
element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage);
const leftImage =
element.$.diff.$.diffTable.querySelector('td.left img');
assert.isNotOk(leftImage);
done();
});
element.$.restAPI.getDiffPreferences().then(prefs => {
element.prefs = prefs;
element.reload();
});
});
});
});
test('delegates cancel()', () => {
element = fixture('basic');
const stub = sandbox.stub(element.$.diff, 'cancel');
element.reload();
assert.isTrue(stub.calledOnce);
@@ -64,7 +478,6 @@ limitations under the License.
});
test('delegates getCursorStops()', () => {
element = fixture('basic');
const returnValue = [document.createElement('b')];
const stub = sandbox.stub(element.$.diff, 'getCursorStops')
.returns(returnValue);
@@ -74,7 +487,6 @@ limitations under the License.
});
test('delegates isRangeSelected()', () => {
element = fixture('basic');
const returnValue = true;
const stub = sandbox.stub(element.$.diff, 'isRangeSelected')
.returns(returnValue);
@@ -84,33 +496,66 @@ limitations under the License.
});
test('delegates toggleLeftDiff()', () => {
element = fixture('basic');
const stub = sandbox.stub(element.$.diff, 'toggleLeftDiff');
element.toggleLeftDiff();
assert.isTrue(stub.calledOnce);
assert.equal(stub.lastCall.args.length, 0);
});
test('delegates loadBlame()', () => {
element = fixture('basic');
const returnValue = Promise.resolve();
const stub = sandbox.stub(element.$.diff, 'loadBlame')
.returns(returnValue);
assert.equal(element.loadBlame(), returnValue);
assert.isTrue(stub.calledOnce);
assert.equal(stub.lastCall.args.length, 0);
});
suite('blame', () => {
setup(() => {
element = fixture('basic');
});
test('delegates clearBlame()', () => {
element = fixture('basic');
const stub = sandbox.stub(element.$.diff, 'clearBlame');
element.clearBlame();
assert.isTrue(stub.calledOnce);
assert.equal(stub.lastCall.args.length, 0);
test('clearBlame', () => {
element._blame = [];
const setBlameSpy = sandbox.spy(element.$.diff.$.diffBuilder, 'setBlame');
element.clearBlame();
assert.isNull(element._blame);
assert.isTrue(setBlameSpy.calledWithExactly(null));
assert.equal(element.isBlameLoaded, false);
});
test('loadBlame', () => {
const mockBlame = [{id: 'commit id', ranges: [{start: 1, end: 2}]}];
const showAlertStub = sinon.stub();
element.addEventListener('show-alert', showAlertStub);
const getBlameStub = sandbox.stub(element.$.restAPI, 'getBlame')
.returns(Promise.resolve(mockBlame));
element.changeNum = 42;
element.patchRange = {patchNum: 5, basePatchNum: 4};
element.path = 'foo/bar.baz';
return element.loadBlame().then(() => {
assert.isTrue(getBlameStub.calledWithExactly(
42, 5, 'foo/bar.baz', true));
assert.isFalse(showAlertStub.called);
assert.equal(element._blame, mockBlame);
assert.equal(element.isBlameLoaded, true);
});
});
test('loadBlame empty', () => {
const mockBlame = [];
const showAlertStub = sinon.stub();
element.addEventListener('show-alert', showAlertStub);
sandbox.stub(element.$.restAPI, 'getBlame')
.returns(Promise.resolve(mockBlame));
element.changeNum = 42;
element.patchRange = {patchNum: 5, basePatchNum: 4};
element.path = 'foo/bar.baz';
return element.loadBlame()
.then(() => {
assert.isTrue(false, 'Promise should not resolve');
})
.catch(() => {
assert.isTrue(showAlertStub.calledOnce);
assert.isNull(element._blame);
assert.equal(element.isBlameLoaded, false);
});
});
});
test('delegates getThreadEls()', () => {
element = fixture('basic');
const returnValue = [document.createElement('b')];
const stub = sandbox.stub(element.$.diff, 'getThreadEls')
.returns(returnValue);
@@ -120,7 +565,6 @@ limitations under the License.
});
test('delegates addDraftAtLine(el)', () => {
element = fixture('basic');
const param0 = document.createElement('b');
const stub = sandbox.stub(element.$.diff, 'addDraftAtLine');
element.addDraftAtLine(param0);
@@ -130,7 +574,6 @@ limitations under the License.
});
test('delegates clearDiffContent()', () => {
element = fixture('basic');
const stub = sandbox.stub(element.$.diff, 'clearDiffContent');
element.clearDiffContent();
assert.isTrue(stub.calledOnce);
@@ -138,7 +581,6 @@ limitations under the License.
});
test('delegates expandAllContext()', () => {
element = fixture('basic');
const stub = sandbox.stub(element.$.diff, 'expandAllContext');
element.expandAllContext();
assert.isTrue(stub.calledOnce);
@@ -146,101 +588,66 @@ limitations under the License.
});
test('passes in changeNum', () => {
element = fixture('basic');
const value = '12345';
element.changeNum = value;
assert.equal(element.$.diff.changeNum, value);
});
test('passes in noAutoRender', () => {
element = fixture('basic');
const value = true;
element.noAutoRender = value;
assert.equal(element.$.diff.noAutoRender, value);
});
test('passes in patchRange', () => {
element = fixture('basic');
const value = {patchNum: 'foo', basePatchNum: 'bar'};
element.patchRange = value;
assert.equal(element.$.diff.patchRange, value);
});
test('passes in path', () => {
element = fixture('basic');
const value = 'some/file/path';
element.path = value;
assert.equal(element.$.diff.path, value);
});
test('passes in prefs', () => {
element = fixture('basic');
const value = {};
element.prefs = value;
assert.equal(element.$.diff.prefs, value);
});
test('passes in projectConfig', () => {
element = fixture('basic');
const value = {};
element.projectConfig = value;
assert.equal(element.$.diff.projectConfig, value);
});
test('passes in changeNum', () => {
element = fixture('basic');
const value = '12345';
element.changeNum = value;
assert.equal(element.$.diff.changeNum, value);
});
test('passes in projectName', () => {
element = fixture('basic');
const value = 'Gerrit';
element.projectName = value;
assert.equal(element.$.diff.projectName, value);
});
test('passes in displayLine', () => {
element = fixture('basic');
const value = true;
element.displayLine = value;
assert.equal(element.$.diff.displayLine, value);
});
test('passes out isImageDiff', () => {
element = fixture('basic');
const value = true;
// isImageDiff is computed, so we cannot just set it.
sandbox.stub(element.$.diff, '_computeIsImageDiff').returns(value);
element.$.diff._diff = {left: [], right: [], content: []};
assert.equal(element.isImageDiff, value);
});
test('passes in commitRange', () => {
element = fixture('basic');
const value = {};
element.commitRange = value;
assert.equal(element.$.diff.commitRange, value);
});
test('passes in filesWeblinks', () => {
element = fixture('basic');
const value = {};
element.filesWeblinks = value;
assert.equal(element.$.diff.filesWeblinks, value);
});
test('passes out filesWeblinks', () => {
element = fixture('basic');
const value = {};
element.$.diff.filesWeblinks = value;
assert.equal(element.filesWeblinks, value);
});
test('passes in hidden', () => {
element = fixture('basic');
const value = true;
element.hidden = value;
assert.equal(element.$.diff.hidden, value);
@@ -248,53 +655,125 @@ limitations under the License.
});
test('passes in noRenderOnPrefsChange', () => {
element = fixture('basic');
const value = true;
element.noRenderOnPrefsChange = value;
assert.equal(element.$.diff.noRenderOnPrefsChange, value);
});
test('passes in comments', () => {
element = fixture('basic');
const value = {left: [], right: []};
element.comments = value;
assert.equal(element.$.diff.comments, value);
});
test('passes in lineWrapping', () => {
element = fixture('basic');
const value = true;
element.lineWrapping = value;
assert.equal(element.$.diff.lineWrapping, value);
});
test('passes in viewMode', () => {
element = fixture('basic');
const value = 'SIDE_BY_SIDE';
element.viewMode = value;
assert.equal(element.$.diff.viewMode, value);
});
test('passes in lineOfInterest', () => {
element = fixture('basic');
const value = {number: 123, leftSide: true};
element.lineOfInterest = value;
assert.equal(element.$.diff.lineOfInterest, value);
});
test('passes in showLoadFailure', () => {
element = fixture('basic');
const value = true;
element.showLoadFailure = value;
assert.equal(element.$.diff.showLoadFailure, value);
});
suite('_reportDiff', () => {
let reportStub;
test('passes out isBlameLoaded', () => {
element = fixture('basic');
const value = true;
sandbox.stub(element.$.diff, '_computeIsBlameLoaded').returns(value);
element.$.diff._blame = {};
assert.equal(element.isBlameLoaded, value);
setup(() => {
element = fixture('basic');
element.patchRange = {basePatchNum: 1};
reportStub = sandbox.stub(element.$.reporting, 'reportInteraction');
});
test('null and content-less', () => {
element._reportDiff(null);
assert.isFalse(reportStub.called);
element._reportDiff({});
assert.isFalse(reportStub.called);
});
test('diff w/ no delta', () => {
const diff = {
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 = {
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 = {
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.equal(reportStub.lastCall.args[0], 'rebase-percent-nonzero');
assert.strictEqual(reportStub.lastCall.args[1], 50);
});
test('diff w/ all rebase delta', () => {
const diff = {content: [{
a: ['foo', 'bar'],
b: ['baz', 'foo'],
due_to_rebase: true,
}]};
element._reportDiff(diff);
assert.isTrue(reportStub.calledOnce);
assert.equal(reportStub.lastCall.args[0], 'rebase-percent-nonzero');
assert.strictEqual(reportStub.lastCall.args[1], 100);
});
test('diff against parent event', () => {
element.patchRange.basePatchNum = 'PARENT';
const diff = {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]);
});
});
});
</script>

View File

@@ -18,9 +18,7 @@ limitations under the License.
<link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
<link rel="import" href="../../../styles/shared-styles.html">
<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
<link rel="import" href="../../shared/gr-button/gr-button.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
<link rel="import" href="../gr-diff-builder/gr-diff-builder.html">
<link rel="import" href="../gr-diff-comment-thread/gr-diff-comment-thread.html">
<link rel="import" href="../gr-diff-highlight/gr-diff-highlight.html">
@@ -270,26 +268,26 @@ limitations under the License.
<div>[[item]]</div>
</template>
</div>
<div class$="[[_computeContainerClass(_loggedIn, viewMode, displayLine)]]"
<div class$="[[_computeContainerClass(loggedIn, viewMode, displayLine)]]"
on-tap="_handleTap">
<gr-diff-selection diff="[[_diff]]">
<gr-diff-selection diff="[[diff]]">
<gr-diff-highlight
id="highlights"
logged-in="[[_loggedIn]]"
logged-in="[[loggedIn]]"
comments="{{comments}}">
<gr-diff-builder
id="diffBuilder"
comments="[[comments]]"
project-name="[[projectName]]"
diff="[[_diff]]"
diff="[[diff]]"
diff-path="[[path]]"
change-num="[[changeNum]]"
patch-num="[[patchRange.patchNum]]"
view-mode="[[viewMode]]"
line-wrapping="[[lineWrapping]]"
is-image-diff="[[isImageDiff]]"
base-image="[[_baseImage]]"
revision-image="[[_revisionImage]]"
base-image="[[baseImage]]"
revision-image="[[revisionImage]]"
parent-index="[[_parentIndex]]"
create-comment-fn="[[_createThreadGroupFn]]"
line-of-interest="[[lineOfInterest]]">
@@ -301,16 +299,16 @@ limitations under the License.
</gr-diff-highlight>
</gr-diff-selection>
</div>
<div class$="[[_computeNewlineWarningClass(_newlineWarning, _loading)]]">
<div class$="[[_computeNewlineWarningClass(_newlineWarning, loading)]]">
[[_newlineWarning]]
</div>
<div id="loadingError" class$="[[_computeErrorClass(_errorMessage)]]">
[[_errorMessage]]
<div id="loadingError" class$="[[_computeErrorClass(errorMessage)]]">
[[errorMessage]]
</div>
<div id="sizeWarning" class$="[[_computeWarningClass(_showWarning)]]">
<p>
Prevented render because "Whole file" is enabled and this diff is very
large (about [[_diffLength(_diff)]] lines).
large (about [[_diffLength(diff)]] lines).
</p>
<gr-button on-tap="_handleLimitedBypass">
Render with limited context
@@ -319,8 +317,6 @@ limitations under the License.
Render anyway (may be slow)
</gr-button>
</div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
<gr-reporting id="reporting" category="diff"></gr-reporting>
</template>
<script src="gr-diff-line.js"></script>
<script src="gr-diff-group.js"></script>

View File

@@ -21,11 +21,6 @@
const ERR_COMMENT_ON_EDIT_BASE = 'You cannot comment on the base patch set ' +
'of an edit.';
const ERR_INVALID_LINE = 'Invalid line number: ';
const MSG_EMPTY_BLAME = 'No blame information for this diff.';
const EVENT_AGAINST_PARENT = 'diff-against-parent';
const EVENT_ZERO_REBASE = 'rebase-percent-zero';
const EVENT_NONZERO_REBASE = 'rebase-percent-nonzero';
const NO_NEWLINE_BASE = 'No newline at end of base file.';
const NO_NEWLINE_REVISION = 'No newline at end of revision file.';
@@ -64,6 +59,12 @@
* @event diff-comments-modified
*/
/**
* Fired when a draft is added or edited.
*
* @event draft-interaction
*/
properties: {
changeNum: String,
noAutoRender: {
@@ -88,15 +89,8 @@
},
isImageDiff: {
type: Boolean,
computed: '_computeIsImageDiff(_diff)',
notify: true,
},
commitRange: Object,
filesWeblinks: {
type: Object,
value() { return {}; },
notify: true,
},
hidden: {
type: Boolean,
reflectToAttribute: true,
@@ -123,38 +117,33 @@
*/
lineOfInterest: Object,
/**
* If the diff fails to load, show the failure message in the diff rather
* than bubbling the error up to the whole page. This is useful for when
* loading inline diffs because one diff failing need not mark the whole
* page with a failure.
*/
showLoadFailure: Boolean,
_loading: {
loading: {
type: Boolean,
value: false,
observer: '_loadingChanged',
},
_loggedIn: {
loggedIn: {
type: Boolean,
value: false,
},
_diff: Object,
diff: {
type: Object,
observer: '_diffChanged',
},
_diffHeaderItems: {
type: Array,
value: [],
computed: '_computeDiffHeaderItems(_diff.*)',
computed: '_computeDiffHeaderItems(diff.*)',
},
_diffTableClass: {
type: String,
value: '',
},
/** @type {?Object} */
_baseImage: Object,
baseImage: Object,
/** @type {?Object} */
_revisionImage: Object,
revisionImage: Object,
/**
* Whether the safety check for large diffs when whole-file is set has
@@ -172,20 +161,16 @@
_showWarning: Boolean,
/** @type {?string} */
_errorMessage: {
errorMessage: {
type: String,
value: null,
},
/** @type {?Object} */
_blame: {
blame: {
type: Object,
value: null,
},
isBlameLoaded: {
type: Boolean,
notify: true,
computed: '_computeIsBlameLoaded(_blame)',
observer: '_blameChanged',
},
_parentIndex: {
@@ -195,7 +180,7 @@
_newlineWarning: {
type: String,
computed: '_computeNewlineWarning(_diff)',
computed: '_computeNewlineWarning(diff)',
},
/**
@@ -220,63 +205,6 @@
'create-comment': '_handleCreateComment',
},
attached() {
this._getLoggedIn().then(loggedIn => {
this._loggedIn = loggedIn;
});
},
ready() {
if (this._canRender()) {
this.reload();
}
},
/** @return {!Promise} */
reload() {
this._loading = true;
this._errorMessage = null;
const diffRequest = this._getDiff()
.then(diff => {
this._reportDiff(diff);
return diff;
})
.catch(e => {
this._handleGetDiffError(e);
return null;
});
const assetRequest = diffRequest.then(diff => {
// If the diff is null, then it's failed to load.
if (!diff) { return null; }
return this._loadDiffAssets(diff);
});
return Promise.all([diffRequest, assetRequest])
.then(results => {
const diff = results[0];
if (!diff) {
return Promise.resolve();
}
this.filesWeblinks = this._getFilesWeblinks(diff);
this._diff = diff;
return new Promise(resolve => {
const callback = () => {
resolve();
this.removeEventListener('render', callback);
};
this.addEventListener('render', callback);
this._renderDiffTable();
});
})
.catch(err => {
console.warn('Error encountered loading diff:', err);
})
.then(() => { this._loading = false; });
},
/** Cancel any remaining diff builder rendering work. */
cancel() {
this.$.diffBuilder.cancel();
@@ -300,37 +228,13 @@
this.toggleClass('no-left');
},
/**
* Load and display blame information for the base of the diff.
* @return {Promise} A promise that resolves when blame finishes rendering.
*/
loadBlame() {
return this.$.restAPI.getBlame(this.changeNum, this.patchRange.patchNum,
this.path, true)
.then(blame => {
if (!blame.length) {
this.fire('show-alert', {message: MSG_EMPTY_BLAME});
return Promise.reject(MSG_EMPTY_BLAME);
}
this._blame = blame;
this.$.diffBuilder.setBlame(blame);
this.classList.add('showBlame');
});
},
_computeIsBlameLoaded(blame) {
return !!blame;
},
/**
* Unload blame information for the diff.
*/
clearBlame() {
this._blame = null;
this.$.diffBuilder.setBlame(null);
this.classList.remove('showBlame');
_blameChanged(newValue) {
this.$.diffBuilder.setBlame(newValue);
if (newValue) {
this.classList.add('showBlame');
} else {
this.classList.remove('showBlame');
}
},
_handleCommentSaveOrDiscard() {
@@ -338,12 +242,6 @@
{bubbles: true}));
},
/** @return {boolean}} */
_canRender() {
return !!this.changeNum && !!this.patchRange && !!this.path &&
!this.noAutoRender;
},
/** @return {!Array<!HTMLElement>} */
getThreadEls() {
let threads = [];
@@ -432,7 +330,7 @@
/** @return {boolean} */
_isValidElForComment(el) {
if (!this._loggedIn) {
if (!this.loggedIn) {
this.fire('show-auth-required');
return false;
}
@@ -461,7 +359,7 @@
* @param {!Object=} opt_range
*/
_createComment(lineEl, opt_lineNum, opt_side, opt_range) {
this.$.reporting.recordDraftInteraction();
this.dispatchEvent(new CustomEvent('draft-interaction', {bubbles: true}));
const contentText = this.$.diffBuilder.getContentByLineEl(lineEl);
const contentEl = contentText.parentElement;
const side = opt_side ||
@@ -681,7 +579,7 @@
_loadingChanged(newValue) {
if (newValue) {
this.cancel();
this.clearBlame();
this._blame = null;
this._safetyBypass = null;
this._showWarning = false;
this.clearDiffContent();
@@ -695,7 +593,7 @@
_prefsChanged(prefs) {
if (!prefs) { return; }
this.clearBlame();
this._blame = null;
const stylesToUpdate = {};
@@ -716,7 +614,13 @@
this.updateStyles(stylesToUpdate);
if (this._diff && this.comments && !this.noRenderOnPrefsChange) {
if (this.diff && this.comments && !this.noRenderOnPrefsChange) {
this._renderDiffTable();
}
},
_diffChanged(newValue) {
if (newValue) {
this._renderDiffTable();
}
},
@@ -727,7 +631,7 @@
return;
}
if (this.prefs.context === -1 &&
this._diffLength(this._diff) >= LARGE_DIFF_THRESHOLD_LINES &&
this._diffLength(this.diff) >= LARGE_DIFF_THRESHOLD_LINES &&
this._safetyBypass === null) {
this._showWarning = true;
this.dispatchEvent(new CustomEvent('render', {bubbles: true}));
@@ -752,138 +656,6 @@
this.$.diffTable.innerHTML = null;
},
_handleGetDiffError(response) {
// Loading the diff may respond with 409 if the file is too large. In this
// case, use a toast error..
if (response.status === 409) {
this.fire('server-error', {response});
return;
}
if (this.showLoadFailure) {
this._errorMessage = [
'Encountered error when loading the diff:',
response.status,
response.statusText,
].join(' ');
return;
}
this.fire('page-error', {response});
},
/** @return {!Promise<!Object>} */
_getDiff() {
// Wrap the diff request in a new promise so that the error handler
// rejects the promise, allowing the error to be handled in the .catch.
return new Promise((resolve, reject) => {
this.$.restAPI.getDiff(
this.changeNum,
this.patchRange.basePatchNum,
this.patchRange.patchNum,
this.path,
reject)
.then(resolve);
});
},
_getFilesWeblinks(diff) {
if (!this.commitRange) { return {}; }
return {
meta_a: Gerrit.Nav.getFileWebLinks(
this.projectName, this.commitRange.baseCommit, this.path,
{weblinks: diff && diff.meta_a && diff.meta_a.web_links}),
meta_b: Gerrit.Nav.getFileWebLinks(
this.projectName, this.commitRange.commit, this.path,
{weblinks: diff && diff.meta_b && diff.meta_b.web_links}),
};
},
/**
* Report info about the diff response.
*/
_reportDiff(diff) {
if (!diff || !diff.content) { return; }
// Count the delta lines stemming from normal deltas, and from
// due_to_rebase deltas.
let nonRebaseDelta = 0;
let rebaseDelta = 0;
diff.content.forEach(chunk => {
if (chunk.ab) { return; }
const deltaSize = Math.max(
chunk.a ? chunk.a.length : 0, chunk.b ? chunk.b.length : 0);
if (chunk.due_to_rebase) {
rebaseDelta += deltaSize;
} else {
nonRebaseDelta += deltaSize;
}
});
// Find the percent of the delta from due_to_rebase chunks rounded to two
// digits. Diffs with no delta are considered 0%.
const totalDelta = rebaseDelta + nonRebaseDelta;
const percentRebaseDelta = !totalDelta ? 0 :
Math.round(100 * rebaseDelta / totalDelta);
// Report the due_to_rebase percentage in the "diff" category when
// applicable.
if (this.patchRange.basePatchNum === 'PARENT') {
this.$.reporting.reportInteraction(EVENT_AGAINST_PARENT);
} else if (percentRebaseDelta === 0) {
this.$.reporting.reportInteraction(EVENT_ZERO_REBASE);
} else {
this.$.reporting.reportInteraction(EVENT_NONZERO_REBASE,
percentRebaseDelta);
}
},
/** @return {!Promise} */
_getLoggedIn() {
return this.$.restAPI.getLoggedIn();
},
/**
* @param {Object} diff
* @return {boolean}
*/
_computeIsImageDiff(diff) {
if (!diff) { return false; }
const isA = diff.meta_a &&
diff.meta_a.content_type.startsWith('image/');
const isB = diff.meta_b &&
diff.meta_b.content_type.startsWith('image/');
return !!(diff.binary && (isA || isB));
},
/**
* @param {Object} diff
* @return {!Promise}
*/
_loadDiffAssets(diff) {
if (this._computeIsImageDiff(diff)) {
return this._getImages(diff).then(images => {
this._baseImage = images.baseImage;
this._revisionImage = images.revisionImage;
});
} else {
this._baseImage = null;
this._revisionImage = null;
return Promise.resolve();
}
},
/**
* @param {Object} diff
* @return {!Promise}
*/
_getImages(diff) {
return this.$.restAPI.getImagesForDiff(this.changeNum, diff,
this.patchRange);
},
_projectConfigChanged(projectConfig) {
const threadEls = this.getThreadEls();
for (let i = 0; i < threadEls.length; i++) {

View File

@@ -48,18 +48,8 @@ limitations under the License.
sandbox.restore();
});
test('reload cancels before network resolves', () => {
element = fixture('basic');
const cancelStub = sandbox.stub(element, 'cancel');
// Stub the network calls into requests that never resolve.
sandbox.stub(element, '_getDiff', () => new Promise(() => {}));
element.reload();
assert.isTrue(cancelStub.called);
});
test('cancel', () => {
element = fixture('basic');
const cancelStub = sandbox.stub(element.$.diffBuilder, 'cancel');
element.cancel();
assert.isTrue(cancelStub.calledOnce);
@@ -208,41 +198,6 @@ limitations under the License.
element.$$('.diffContainer').classList.contains('displayLine'));
});
test('loads files weblinks', () => {
const weblinksStub = sandbox.stub(Gerrit.Nav, '_generateWeblinks')
.returns({name: 'stubb', url: '#s'});
sandbox.stub(element.$.restAPI, 'getDiff').returns(Promise.resolve({
content: [],
}));
element.projectName = 'test-project';
element.path = 'test-path';
element.commitRange = {baseCommit: 'test-base', commit: 'test-commit'};
element.patchRange = {};
return element.reload().then(() => {
assert.isTrue(weblinksStub.calledTwice);
assert.isTrue(weblinksStub.firstCall.calledWith({
commit: 'test-base',
file: 'test-path',
options: {
weblinks: undefined,
},
repo: 'test-project',
type: Gerrit.Nav.WeblinkType.FILE}));
assert.isTrue(weblinksStub.secondCall.calledWith({
commit: 'test-commit',
file: 'test-path',
options: {
weblinks: undefined,
},
repo: 'test-project',
type: Gerrit.Nav.WeblinkType.FILE}));
assert.deepEqual(element.filesWeblinks, {
meta_a: [{name: 'stubb', url: '#s'}],
meta_b: [{name: 'stubb', url: '#s'}],
});
});
});
test('remove comment', () => {
element.comments = {
meta: {
@@ -408,66 +363,29 @@ limitations under the License.
'wsAAAAAAAAAAAAA/////w==',
type: 'image/bmp',
};
const mockCommit = {
commit: '9a1a1d10baece5efbba10bc4ccf808a67a50ac0a',
parents: [{
commit: '7338aa9adfe57909f1fdaf88975cdea467d3382f',
subject: 'Added a carrot',
}],
author: {
name: 'Wyatt Allen',
email: 'wyatta@google.com',
date: '2016-05-23 21:44:51.000000000',
tz: -420,
},
committer: {
name: 'Wyatt Allen',
email: 'wyatta@google.com',
date: '2016-05-25 00:25:41.000000000',
tz: -420,
},
subject: 'Updated the carrot',
message: 'Updated the carrot\n\nChange-Id: Iabcd123\n',
};
const mockComments = {baseComments: [], comments: []};
sandbox.stub(element.$.restAPI, 'getCommitInfo')
.returns(Promise.resolve(mockCommit));
sandbox.stub(element.$.restAPI,
'getB64FileContents',
(changeId, patchNum, path, opt_parentIndex) => {
return Promise.resolve(opt_parentIndex === 1 ? mockFile1 :
mockFile2);
});
sandbox.stub(element.$.restAPI, '_getDiffComments')
.returns(Promise.resolve(mockComments));
sandbox.stub(element.$.restAPI, 'getDiffDrafts')
.returns(Promise.resolve(mockComments));
element.patchRange = {basePatchNum: 'PARENT', patchNum: 1};
element.comments = {left: [], right: []};
element.isImageDiff = true;
element.prefs = {
auto_hide_diff_table_header: true,
context: 10,
cursor_blink_rate: 0,
font_size: 12,
ignore_whitespace: 'IGNORE_NONE',
intraline_difference: true,
line_length: 100,
line_wrapping: false,
show_line_endings: true,
show_tabs: true,
show_whitespace_errors: true,
syntax_highlighting: true,
tab_size: 8,
theme: 'DEFAULT',
};
});
test('renders image diffs with same file name', done => {
const mockDiff = {
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,
};
sandbox.stub(element.$.restAPI, 'getDiff')
.returns(Promise.resolve(mockDiff));
const rendered = () => {
// Recognizes that it should be an image diff.
assert.isTrue(element.isImageDiff);
@@ -522,10 +440,24 @@ limitations under the License.
element.addEventListener('render', rendered);
element.$.restAPI.getDiffPreferences().then(prefs => {
element.prefs = prefs;
element.reload();
});
element.baseImage = mockFile1;
element.revisionImage = mockFile2;
element.diff = {
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,
};
});
test('renders image diffs with a different file name', done => {
@@ -545,8 +477,6 @@ limitations under the License.
content: [{skip: 66}],
binary: true,
};
sandbox.stub(element.$.restAPI, 'getDiff')
.returns(Promise.resolve(mockDiff));
const rendered = () => {
// Recognizes that it should be an image diff.
@@ -604,10 +534,11 @@ limitations under the License.
element.addEventListener('render', rendered);
element.$.restAPI.getDiffPreferences().then(prefs => {
element.prefs = prefs;
element.reload();
});
element.baseImage = mockFile1;
element.baseImage._name = mockDiff.meta_a.name;
element.revisionImage = mockFile2;
element.revisionImage._name = mockDiff.meta_b.name;
element.diff = mockDiff;
});
test('renders added image', done => {
@@ -626,8 +557,6 @@ limitations under the License.
content: [{skip: 66}],
binary: true,
};
sandbox.stub(element.$.restAPI, 'getDiff')
.returns(Promise.resolve(mockDiff));
element.addEventListener('render', () => {
// Recognizes that it should be an image diff.
@@ -643,10 +572,8 @@ limitations under the License.
done();
});
element.$.restAPI.getDiffPreferences().then(prefs => {
element.prefs = prefs;
element.reload();
});
element.revisionImage = mockFile2;
element.diff = mockDiff;
});
test('renders removed image', done => {
@@ -665,8 +592,6 @@ limitations under the License.
content: [{skip: 66}],
binary: true,
};
sandbox.stub(element.$.restAPI, 'getDiff')
.returns(Promise.resolve(mockDiff));
element.addEventListener('render', () => {
// Recognizes that it should be an image diff.
@@ -682,10 +607,8 @@ limitations under the License.
done();
});
element.$.restAPI.getDiffPreferences().then(prefs => {
element.prefs = prefs;
element.reload();
});
element.baseImage = mockFile1;
element.diff = mockDiff;
});
test('does not render disallowed image type', done => {
@@ -706,9 +629,6 @@ limitations under the License.
};
mockFile1.type = 'image/jpeg-evil';
sandbox.stub(element.$.restAPI, 'getDiff')
.returns(Promise.resolve(mockDiff));
element.addEventListener('render', () => {
// Recognizes that it should be an image diff.
assert.isTrue(element.isImageDiff);
@@ -719,10 +639,8 @@ limitations under the License.
done();
});
element.$.restAPI.getDiffPreferences().then(prefs => {
element.prefs = prefs;
element.reload();
});
element.baseImage = mockFile1;
element.diff = mockDiff;
});
});
@@ -769,67 +687,10 @@ limitations under the License.
content.click();
});
test('_getDiff handles null diff responses', done => {
stub('gr-rest-api-interface', {
getDiff() { return Promise.resolve(null); },
});
element.changeNum = 123;
element.patchRange = {basePatchNum: 1, patchNum: 2};
element.path = 'file.txt';
element._getDiff().then(done);
});
test('reload resolves on error', () => {
const onErrStub = sandbox.stub(element, '_handleGetDiffError');
const error = {ok: false, status: 500};
sandbox.stub(element.$.restAPI, 'getDiff',
(changeNum, basePatchNum, patchNum, path, onErr) => {
onErr(error);
});
return element.reload().then(() => {
assert.isTrue(onErrStub.calledOnce);
});
});
suite('_handleGetDiffError', () => {
let serverErrorStub;
let pageErrorStub;
setup(() => {
serverErrorStub = sinon.stub();
element.addEventListener('server-error', serverErrorStub);
pageErrorStub = sinon.stub();
element.addEventListener('page-error', pageErrorStub);
});
test('page error on HTTP-409', () => {
element._handleGetDiffError({status: 409});
assert.isTrue(serverErrorStub.calledOnce);
assert.isFalse(pageErrorStub.called);
assert.isNotOk(element._errorMessage);
});
test('server error on non-HTTP-409', () => {
element._handleGetDiffError({status: 500});
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!'});
assert.isFalse(serverErrorStub.called);
assert.isFalse(pageErrorStub.called);
assert.equal(element._errorMessage,
'Encountered error when loading the diff: 500 Failure!');
});
});
suite('getCursorStops', () => {
const setupDiff = function() {
const mock = document.createElement('mock-diff-response');
element._diff = mock.diffResponse;
element.diff = mock.diffResponse;
element.comments = {
left: [],
right: [],
@@ -878,15 +739,8 @@ limitations under the License.
suite('logged in', () => {
let fakeLineEl;
setup(() => {
const getLoggedInPromise = Promise.resolve(true);
stub('gr-rest-api-interface', {
getLoggedIn() { return getLoggedInPromise; },
getPreferences() {
return Promise.resolve({time_format: 'HHMM_12'});
},
getAccountCapabilities() { return Promise.resolve(); },
});
element = fixture('basic');
element.loggedIn = true;
element.patchRange = {};
fakeLineEl = {
@@ -895,15 +749,11 @@ limitations under the License.
contains: sandbox.stub().returns(true),
},
};
return getLoggedInPromise;
});
test('addDraftAtLine', () => {
sandbox.stub(element, '_selectLine');
sandbox.stub(element, '_createComment');
const loggedInErrorSpy = sandbox.spy();
element.addEventListener('show-auth-required', loggedInErrorSpy);
assert.isFalse(loggedInErrorSpy.called);
element.addDraftAtLine(fakeLineEl);
assert.isTrue(element._createComment
.calledWithExactly(fakeLineEl, 42));
@@ -913,12 +763,9 @@ limitations under the License.
element.patchRange.basePatchNum = element.EDIT_NAME;
sandbox.stub(element, '_selectLine');
sandbox.stub(element, '_createComment');
const loggedInErrorSpy = sandbox.spy();
const alertSpy = sandbox.spy();
element.addEventListener('show-auth-required', loggedInErrorSpy);
element.addEventListener('show-alert', alertSpy);
element.addDraftAtLine(fakeLineEl);
assert.isFalse(loggedInErrorSpy.called);
assert.isTrue(alertSpy.called);
assert.isFalse(element._createComment.called);
});
@@ -928,19 +775,16 @@ limitations under the License.
element.patchRange.basePatchNum = element.PARENT_NAME;
sandbox.stub(element, '_selectLine');
sandbox.stub(element, '_createComment');
const loggedInErrorSpy = sandbox.spy();
const alertSpy = sandbox.spy();
element.addEventListener('show-auth-required', loggedInErrorSpy);
element.addEventListener('show-alert', alertSpy);
element.addDraftAtLine(fakeLineEl);
assert.isFalse(loggedInErrorSpy.called);
assert.isTrue(alertSpy.called);
assert.isFalse(element._createComment.called);
});
suite('change in preferences', () => {
setup(() => {
element._diff = {
element.diff = {
meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg',
lines: 560},
@@ -1074,7 +918,8 @@ limitations under the License.
suite('diff header', () => {
setup(() => {
element._diff = {
element = fixture('basic');
element.diff = {
meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg',
lines: 560},
@@ -1087,15 +932,15 @@ limitations under the License.
test('hidden', () => {
assert.equal(element._diffHeaderItems.length, 0);
element.push('_diff.diff_header', 'diff --git a/test.jpg b/test.jpg');
element.push('diff.diff_header', 'diff --git a/test.jpg b/test.jpg');
assert.equal(element._diffHeaderItems.length, 0);
element.push('_diff.diff_header', 'index 2adc47d..f9c2f2c 100644');
element.push('diff.diff_header', 'index 2adc47d..f9c2f2c 100644');
assert.equal(element._diffHeaderItems.length, 0);
element.push('_diff.diff_header', '--- a/test.jpg');
element.push('diff.diff_header', '--- a/test.jpg');
assert.equal(element._diffHeaderItems.length, 0);
element.push('_diff.diff_header', '+++ b/test.jpg');
element.push('diff.diff_header', '+++ b/test.jpg');
assert.equal(element._diffHeaderItems.length, 0);
element.push('_diff.diff_header', 'test');
element.push('diff.diff_header', 'test');
assert.equal(element._diffHeaderItems.length, 1);
flushAsynchronousOperations();
@@ -1103,13 +948,13 @@ limitations under the License.
});
test('binary files', () => {
element._diff.binary = true;
element.diff.binary = true;
assert.equal(element._diffHeaderItems.length, 0);
element.push('_diff.diff_header', 'diff --git a/test.jpg b/test.jpg');
element.push('diff.diff_header', 'diff --git a/test.jpg b/test.jpg');
assert.equal(element._diffHeaderItems.length, 0);
element.push('_diff.diff_header', 'test');
element.push('diff.diff_header', 'test');
assert.equal(element._diffHeaderItems.length, 1);
element.push('_diff.diff_header', 'Binary files differ');
element.push('diff.diff_header', 'Binary files differ');
assert.equal(element._diffHeaderItems.length, 1);
});
});
@@ -1126,7 +971,7 @@ limitations under the License.
new CustomEvent('render', {bubbles: true}));
});
const mock = document.createElement('mock-diff-response');
element._diff = mock.diffResponse;
element.diff = mock.diffResponse;
element.comments = {left: [], right: []};
element.noRenderOnPrefsChange = true;
});
@@ -1171,144 +1016,19 @@ limitations under the License.
element = fixture('basic');
});
test('clearBlame', () => {
element._blame = [];
test('unsetting', () => {
element.blame = [];
const setBlameSpy = sandbox.spy(element.$.diffBuilder, 'setBlame');
element.classList.add('showBlame');
element.clearBlame();
assert.isNull(element._blame);
element.blame = null;
assert.isTrue(setBlameSpy.calledWithExactly(null));
assert.isFalse(element.classList.contains('showBlame'));
});
test('loadBlame', () => {
test('setting', () => {
const mockBlame = [{id: 'commit id', ranges: [{start: 1, end: 2}]}];
const showAlertStub = sinon.stub();
element.addEventListener('show-alert', showAlertStub);
const getBlameStub = sandbox.stub(element.$.restAPI, 'getBlame')
.returns(Promise.resolve(mockBlame));
element.changeNum = 42;
element.patchRange = {patchNum: 5, basePatchNum: 4};
element.path = 'foo/bar.baz';
return element.loadBlame().then(() => {
assert.isTrue(getBlameStub.calledWithExactly(
42, 5, 'foo/bar.baz', true));
assert.isFalse(showAlertStub.called);
assert.equal(element._blame, mockBlame);
assert.isTrue(element.classList.contains('showBlame'));
});
});
test('loadBlame empty', () => {
const mockBlame = [];
const showAlertStub = sinon.stub();
element.addEventListener('show-alert', showAlertStub);
sandbox.stub(element.$.restAPI, 'getBlame')
.returns(Promise.resolve(mockBlame));
element.changeNum = 42;
element.patchRange = {patchNum: 5, basePatchNum: 4};
element.path = 'foo/bar.baz';
return element.loadBlame()
.then(() => {
assert.isTrue(false, 'Promise should not resolve');
})
.catch(() => {
assert.isTrue(showAlertStub.calledOnce);
assert.isNull(element._blame);
assert.isFalse(element.classList.contains('showBlame'));
});
});
});
suite('_reportDiff', () => {
let reportStub;
setup(() => {
element = fixture('basic');
element.patchRange = {basePatchNum: 1};
reportStub = sandbox.stub(element.$.reporting, 'reportInteraction');
});
test('null and content-less', () => {
element._reportDiff(null);
assert.isFalse(reportStub.called);
element._reportDiff({});
assert.isFalse(reportStub.called);
});
test('diff w/ no delta', () => {
const diff = {
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 = {
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 = {
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.equal(reportStub.lastCall.args[0], 'rebase-percent-nonzero');
assert.strictEqual(reportStub.lastCall.args[1], 50);
});
test('diff w/ all rebase delta', () => {
const diff = {content: [{
a: ['foo', 'bar'],
b: ['baz', 'foo'],
due_to_rebase: true,
}]};
element._reportDiff(diff);
assert.isTrue(reportStub.calledOnce);
assert.equal(reportStub.lastCall.args[0], 'rebase-percent-nonzero');
assert.strictEqual(reportStub.lastCall.args[1], 100);
});
test('diff against parent event', () => {
element.patchRange.basePatchNum = 'PARENT';
const diff = {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]);
element.blame = mockBlame;
assert.isTrue(element.classList.contains('showBlame'));
});
});