Becky Siegel 55a1eec37d Fix jumping scroll position when diff view loads
Previously, there was an issue where if you started scrolling on a diff
view page before everything was loaded, the page would jump back to the
top after loading finished.

This change temporarily adjusts scroll behavior when scrolling is
detected mid-diff load. The scroll behavior is then restored after
loading completes.

Bug: Issue 4411
Change-Id: I8175d433632fd8710f1353f7ba5635b826151ce0
2016-10-06 07:57:41 -07:00

769 lines
25 KiB
HTML

<!DOCTYPE html>
<!--
Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-diff-builder</title>
<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<script src="../../../scripts/util.js"></script>
<script src="../gr-diff/gr-diff-line.js"></script>
<script src="../gr-diff/gr-diff-group.js"></script>
<script src="../gr-diff-highlight/gr-annotation.js"></script>
<script src="gr-diff-builder.js"></script>
<link rel="import" href="../../shared/gr-rest-api-interface/mock-diff-response_test.html">
<link rel="import" href="gr-diff-builder.html">
<test-fixture id="basic">
<template>
<gr-diff-builder>
<table id="diffTable"></table>
</gr-diff-builder>
</template>
</test-fixture>
<test-fixture id="div-with-text">
<template>
<div>Lorem ipsum dolor sit amet, suspendisse inceptos vehicula</div>
</template>
</test-fixture>
<test-fixture id="mock-diff">
<template>
<gr-diff-builder view-mode="SIDE_BY_SIDE">
<table id="diffTable"></table>
</gr-diff-builder>
</template>
</test-fixture>
<script>
suite('gr-diff-builder tests', function() {
var element;
var builder;
setup(function() {
var prefs = {
line_length: 10,
show_tabs: true,
tab_size: 4,
};
builder = new GrDiffBuilder({content: []}, {left: [], right: []}, prefs);
});
test('context control buttons', function() {
var section = {};
var line = {contextGroup: {lines: []}};
// Create 10 lines.
for (var i = 0; i < 10; i++) {
line.contextGroup.lines.push('lorem upsum');
}
// Does not include +10 buttons when there are fewer than 11 lines.
var td = builder._createContextControl(section, line);
var buttons = td.querySelectorAll('gr-button.showContext');
assert.equal(buttons.length, 1);
assert.equal(buttons[0].textContent, 'Show 10 common lines');
// Add another line.
line.contextGroup.lines.push('lorem upsum');
// Includes +10 buttons when there are at least 11 lines.
td = builder._createContextControl(section, line);
buttons = td.querySelectorAll('gr-button.showContext');
assert.equal(buttons.length, 3);
assert.equal(buttons[0].textContent, '+10↑');
assert.equal(buttons[1].textContent, 'Show 11 common lines');
assert.equal(buttons[2].textContent, '+10↓');
});
test('newlines', function() {
var text = 'abcdef';
assert.equal(builder._addNewlines(text, text), text);
text = 'a'.repeat(20);
assert.equal(builder._addNewlines(text, text),
'a'.repeat(10) +
GrDiffBuilder.LINE_FEED_HTML +
'a'.repeat(10));
text = '<span class="thumbsup">👍</span>';
var html = '&lt;span class=&quot;thumbsup&quot;&gt;👍&lt;&#x2F;span&gt;';
assert.equal(builder._addNewlines(text, html),
'&lt;span clas' +
GrDiffBuilder.LINE_FEED_HTML +
's=&quot;thumbsu' +
GrDiffBuilder.LINE_FEED_HTML +
'p&quot;&gt;👍&lt;&#x2F;spa' +
GrDiffBuilder.LINE_FEED_HTML +
'n&gt;');
text = '01234\t56789';
assert.equal(builder._addNewlines(text, text),
'01234\t5' +
GrDiffBuilder.LINE_FEED_HTML +
'6789');
});
test('text length with tabs and unicode', function() {
assert.equal(builder._textLength('12345', 4), 5);
assert.equal(builder._textLength('\t\t12', 4), 10);
assert.equal(builder._textLength('abc💢123', 4), 7);
assert.equal(builder._textLength('abc\t', 8), 8);
assert.equal(builder._textLength('abc\t\t', 10), 20);
assert.equal(builder._textLength('', 10), 0);
assert.equal(builder._textLength('', 10), 0);
assert.equal(builder._textLength('abc\tde', 10), 12);
assert.equal(builder._textLength('abc\tde\t', 10), 20);
assert.equal(builder._textLength('\t\t\t\t\t', 20), 100);
});
test('tab wrapper insertion', function() {
var html = 'abc\tdef';
var wrapper = builder._getTabWrapper(
builder._prefs.tab_size - 3,
builder._prefs.show_tabs);
assert.ok(wrapper);
assert.isAbove(wrapper.length, 0);
assert.equal(builder._addTabWrappers(html, builder._prefs.tab_size),
'abc' + wrapper + 'def');
assert.throws(builder._getTabWrapper.bind(
builder,
// using \x3c instead of < in string so gjslint can parse
'">\x3cimg src="/" onerror="alert(1);">\x3cspan class="',
true));
});
test('comments', function() {
var line = new GrDiffLine(GrDiffLine.Type.BOTH);
line.beforeNumber = 3;
line.afterNumber = 5;
var comments = {left: [], right: []};
assert.deepEqual(builder._getCommentsForLine(comments, line), []);
assert.deepEqual(builder._getCommentsForLine(comments, line,
GrDiffBuilder.Side.LEFT), []);
assert.deepEqual(builder._getCommentsForLine(comments, line,
GrDiffBuilder.Side.RIGHT), []);
comments = {
left: [
{id: 'l3', line: 3},
{id: 'l5', line: 5},
],
right: [
{id: 'r3', line: 3},
{id: 'r5', line: 5},
],
};
assert.deepEqual(builder._getCommentsForLine(comments, line),
[{id: 'l3', line: 3}, {id: 'r5', line: 5}]);
assert.deepEqual(builder._getCommentsForLine(comments, line,
GrDiffBuilder.Side.LEFT), [{id: 'l3', line: 3}]);
assert.deepEqual(builder._getCommentsForLine(comments, line,
GrDiffBuilder.Side.RIGHT), [{id: 'r5', line: 5}]);
});
test('comment thread creation', function() {
var l3 = {id: 'l3', line: 3, updated: '2016-08-09 00:42:32.000000000'};
var l5 = {id: 'l5', line: 5, updated: '2016-08-09 00:42:32.000000000'};
var r5 = {id: 'r5', line: 5, updated: '2016-08-09 00:42:32.000000000'};
builder._comments = {
meta: {
changeNum: '42',
patchRange: {
basePatchNum: 'PARENT',
patchNum: '3',
},
path: '/path/to/foo',
projectConfig: {foo: 'bar'},
},
left: [l3, l5],
right: [r5],
};
function checkThreadProps(threadEl, patchNum, side, comments) {
assert.equal(threadEl.changeNum, '42');
assert.equal(threadEl.patchNum, patchNum);
assert.equal(threadEl.path, '/path/to/foo');
assert.equal(threadEl.side, side);
assert.deepEqual(threadEl.projectConfig, {foo: 'bar'});
assert.deepEqual(threadEl.comments, comments);
}
var line = new GrDiffLine(GrDiffLine.Type.BOTH);
line.beforeNumber = 5;
line.afterNumber = 5;
var threadEl = builder._commentThreadForLine(line);
checkThreadProps(threadEl, '3', 'REVISION', [l5, r5]);
threadEl = builder._commentThreadForLine(line, GrDiffBuilder.Side.RIGHT);
checkThreadProps(threadEl, '3', 'REVISION', [r5]);
threadEl = builder._commentThreadForLine(line, GrDiffBuilder.Side.LEFT);
checkThreadProps(threadEl, '3', 'PARENT', [l5]);
builder._comments.meta.patchRange.basePatchNum = '1';
threadEl = builder._commentThreadForLine(line);
checkThreadProps(threadEl, '3', 'REVISION', [l5, r5]);
threadEl = builder._commentThreadForLine(line, GrDiffBuilder.Side.LEFT);
checkThreadProps(threadEl, '1', 'REVISION', [l5]);
threadEl = builder._commentThreadForLine(line, GrDiffBuilder.Side.RIGHT);
checkThreadProps(threadEl, '3', 'REVISION', [r5]);
builder._comments.meta.patchRange.basePatchNum = 'PARENT';
line = new GrDiffLine(GrDiffLine.Type.REMOVE);
line.beforeNumber = 5;
line.afterNumber = 5;
threadEl = builder._commentThreadForLine(line);
checkThreadProps(threadEl, '3', 'PARENT', [l5, r5]);
line = new GrDiffLine(GrDiffLine.Type.ADD);
line.beforeNumber = 3;
line.afterNumber = 5;
threadEl = builder._commentThreadForLine(line);
checkThreadProps(threadEl, '3', 'REVISION', [l3, r5]);
});
suite('intraline differences', function() {
var el;
var str;
var annotateElementSpy;
var layer;
function slice(str, start, end) {
return Array.from(str).slice(start, end).join('');
}
setup(function() {
el = fixture('div-with-text');
str = el.textContent;
annotateElementSpy = sinon.spy(GrAnnotation, 'annotateElement');
layer = document.createElement('gr-diff-builder')
._createIntralineLayer();
});
teardown(function() {
annotateElementSpy.restore();
});
test('annotate no highlights', function() {
var line = {
text: str,
highlights: [],
};
layer.annotate(el, line);
// The content is unchanged.
assert.isFalse(annotateElementSpy.called);
assert.equal(el.childNodes.length, 1);
assert.instanceOf(el.childNodes[0], Text);
assert.equal(str, el.childNodes[0].textContent);
});
test('annotate with highlights', function() {
var line = {
text: str,
highlights: [
{startIndex: 6, endIndex: 12},
{startIndex: 18, endIndex: 22},
],
};
var str0 = slice(str, 0, 6);
var str1 = slice(str, 6, 12);
var str2 = slice(str, 12, 18);
var str3 = slice(str, 18, 22);
var str4 = slice(str, 22);
layer.annotate(el, line);
assert.isTrue(annotateElementSpy.called);
assert.equal(el.childNodes.length, 5);
assert.instanceOf(el.childNodes[0], Text);
assert.equal(el.childNodes[0].textContent, str0);
assert.notInstanceOf(el.childNodes[1], Text);
assert.equal(el.childNodes[1].textContent, str1);
assert.instanceOf(el.childNodes[2], Text);
assert.equal(el.childNodes[2].textContent, str2);
assert.notInstanceOf(el.childNodes[3], Text);
assert.equal(el.childNodes[3].textContent, str3);
assert.instanceOf(el.childNodes[4], Text);
assert.equal(el.childNodes[4].textContent, str4);
});
test('annotate without endIndex', function() {
var line = {
text: str,
highlights: [
{startIndex: 28},
],
};
var str0 = slice(str, 0, 28);
var str1 = slice(str, 28);
layer.annotate(el, line);
assert.isTrue(annotateElementSpy.called);
assert.equal(el.childNodes.length, 2);
assert.instanceOf(el.childNodes[0], Text);
assert.equal(el.childNodes[0].textContent, str0);
assert.notInstanceOf(el.childNodes[1], Text);
assert.equal(el.childNodes[1].textContent, str1);
});
test('annotate ignores empty highlights', function() {
var line = {
text: str,
highlights: [
{startIndex: 28, endIndex: 28},
],
};
layer.annotate(el, line);
assert.isFalse(annotateElementSpy.called);
assert.equal(el.childNodes.length, 1);
});
test('annotate handles unicode', function() {
// Put some unicode into the string:
str = str.replace(/\s/g, '💢');
el.textContent = str;
var line = {
text: str,
highlights: [
{startIndex: 6, endIndex: 12},
],
};
var str0 = slice(str, 0, 6);
var str1 = slice(str, 6, 12);
var str2 = slice(str, 12);
layer.annotate(el, line);
assert.isTrue(annotateElementSpy.called);
assert.equal(el.childNodes.length, 3);
assert.instanceOf(el.childNodes[0], Text);
assert.equal(el.childNodes[0].textContent, str0);
assert.notInstanceOf(el.childNodes[1], Text);
assert.equal(el.childNodes[1].textContent, str1);
assert.instanceOf(el.childNodes[2], Text);
assert.equal(el.childNodes[2].textContent, str2);
});
test('annotate handles unicode w/o endIndex', function() {
// Put some unicode into the string:
str = str.replace(/\s/g, '💢');
el.textContent = str;
var line = {
text: str,
highlights: [
{startIndex: 6},
],
};
var str0 = slice(str, 0, 6);
var str1 = slice(str, 6);
layer.annotate(el, line);
assert.isTrue(annotateElementSpy.called);
assert.equal(el.childNodes.length, 2);
assert.instanceOf(el.childNodes[0], Text);
assert.equal(el.childNodes[0].textContent, str0);
assert.notInstanceOf(el.childNodes[1], Text);
assert.equal(el.childNodes[1].textContent, str1);
});
});
suite('tab indicators', function() {
var sandbox;
var element;
var layer;
setup(function() {
sandbox = sinon.sandbox.create();
element = fixture('basic');
element._showTabs = true;
layer = element._createTabIndicatorLayer();
});
teardown(function() {
sandbox.restore();
});
test('does nothing with empty line', function() {
var line = {text: ''};
var el = document.createElement('div');
var annotateElementStub = sandbox.stub(GrAnnotation, 'annotateElement');
layer.annotate(el, line);
assert.isFalse(annotateElementStub.called);
});
test('does nothing with no tabs', function() {
var str = 'lorem ipsum no tabs';
var line = {text: str};
var el = document.createElement('div');
el.textContent = str;
var annotateElementStub = sandbox.stub(GrAnnotation, 'annotateElement');
layer.annotate(el, line);
assert.isFalse(annotateElementStub.called);
});
test('annotates tab at beginning', function() {
var str = '\tlorem upsum';
var line = {text: str};
var el = document.createElement('div');
el.textContent = str;
var annotateElementStub = sandbox.stub(GrAnnotation, 'annotateElement');
layer.annotate(el, line);
assert.equal(annotateElementStub.callCount, 1);
var args = annotateElementStub.getCalls()[0].args;
assert.equal(args[0], el);
assert.equal(args[1], 0, 'offset of tab indicator');
assert.equal(args[2], 1, 'length of tab indicator');
assert.include(args[3], 'tab-indicator');
});
test('does not annotate when disabled', function() {
element._showTabs = false;
var str = '\tlorem upsum';
var line = {text: str};
var el = document.createElement('div');
el.textContent = str;
var annotateElementStub = sandbox.stub(GrAnnotation, 'annotateElement');
layer.annotate(el, line);
assert.isFalse(annotateElementStub.called);
});
test('annotates multiple in beginning', function() {
var str = '\t\tlorem upsum';
var line = {text: str};
var el = document.createElement('div');
el.textContent = str;
var annotateElementStub = sandbox.stub(GrAnnotation, 'annotateElement');
layer.annotate(el, line);
assert.equal(annotateElementStub.callCount, 2);
var args = annotateElementStub.getCalls()[0].args;
assert.equal(args[0], el);
assert.equal(args[1], 0, 'offset of tab indicator');
assert.equal(args[2], 1, 'length of tab indicator');
assert.include(args[3], 'tab-indicator');
args = annotateElementStub.getCalls()[1].args;
assert.equal(args[0], el);
assert.equal(args[1], 1, 'offset of tab indicator');
assert.equal(args[2], 1, 'length of tab indicator');
assert.include(args[3], 'tab-indicator');
});
test('annotates intermediate tabs', function() {
var str = 'lorem\tupsum';
var line = {text: str};
var el = document.createElement('div');
el.textContent = str;
var annotateElementStub = sandbox.stub(GrAnnotation, 'annotateElement');
layer.annotate(el, line);
assert.equal(annotateElementStub.callCount, 1);
var args = annotateElementStub.getCalls()[0].args;
assert.equal(args[0], el);
assert.equal(args[1], 5, 'offset of tab indicator');
assert.equal(args[2], 1, 'length of tab indicator');
assert.include(args[3], 'tab-indicator');
});
});
suite('rendering', function() {
var content;
var outputEl;
setup(function(done) {
var prefs = {
line_length: 10,
show_tabs: true,
tab_size: 4,
context: -1
};
content = [
{
a: ['all work and no play make andybons a dull boy'],
b: ['elgoog elgoog elgoog']
},
{
ab: [
'Non eram nescius, Brute, cum, quae summis ingeniis ',
'exquisitaque doctrina philosophi Graeco sermone tractavissent',
]
},
];
stub('gr-reporting', {
time: sinon.stub(),
timeEnd: sinon.stub(),
});
element = fixture('basic');
outputEl = element.queryEffectiveChildren('#diffTable');
element.addEventListener('render', function() {
done();
});
sinon.stub(element, '_getDiffBuilder', function() {
var builder = new GrDiffBuilder(
{content: content}, {left: [], right: []}, prefs, outputEl);
builder.buildSectionElement = function(group) {
var section = document.createElement('stub');
section.textContent = group.lines.reduce(function(acc, line) {
return acc + line.text;
}, '');
return section;
};
return builder;
});
element.diff = {content: content};
element.render({left: [], right: []}, prefs);
});
test('reporting', function(done) {
var timeStub = element.$.reporting.time;
var timeEndStub = element.$.reporting.timeEnd;
flush(function() {
assert.isTrue(timeStub.calledWithExactly('Diff Total Render'));
assert.isTrue(timeStub.calledWithExactly('Diff Content Render'));
assert.isTrue(timeStub.calledWithExactly('Diff Syntax Render'));
assert.isTrue(timeEndStub.calledWithExactly('Diff Total Render'));
assert.isTrue(timeEndStub.calledWithExactly('Diff Content Render'));
assert.isTrue(timeEndStub.calledWithExactly('Diff Syntax Render'));
done();
});
});
test('renderSection', function() {
var section = outputEl.querySelector('stub:nth-of-type(2)');
var prevInnerHTML = section.innerHTML;
section.innerHTML = 'wiped';
element._builder.renderSection(section);
section = outputEl.querySelector('stub:nth-of-type(2)');
assert.equal(section.innerHTML, prevInnerHTML);
});
test('getSectionsByLineRange one line', function() {
var section = outputEl.querySelector('stub:nth-of-type(2)');
var sections = element._builder.getSectionsByLineRange(1, 1, 'left');
assert.equal(sections.length, 1);
assert.strictEqual(sections[0], section);
});
test('getSectionsByLineRange over diff', function() {
var section = [
outputEl.querySelector('stub:nth-of-type(2)'),
outputEl.querySelector('stub:nth-of-type(3)'),
];
var sections = element._builder.getSectionsByLineRange(1, 2, 'left');
assert.equal(sections.length, 2);
assert.strictEqual(sections[0], section[0]);
assert.strictEqual(sections[1], section[1]);
});
test('render-start and render are fired', function() {
var fireStub = sinon.stub(element, 'fire');
element.render({left: [], right: []}, {});
flush(function() {
assert.isTrue(fireStub.calledWithExactly('render-start'));
assert.isTrue(fireStub.calledWithExactly('render'));
done();
});
});
});
suite('mock-diff', function() {
var element;
var builder;
var diff;
var prefs;
setup(function(done) {
element = fixture('mock-diff');
diff = document.createElement('mock-diff-response').diffResponse;
element.diff = diff;
prefs = {
line_length: 80,
show_tabs: true,
tab_size: 4,
};
element.render({left: [], right: []}, prefs).then(function() {
builder = element._builder;
done();
});
});
test('getContentByLine', function() {
var actual;
actual = builder.getContentByLine(2, 'left');
assert.equal(actual.textContent, diff.content[0].ab[1]);
actual = builder.getContentByLine(2, 'right');
assert.equal(actual.textContent, diff.content[0].ab[1]);
actual = builder.getContentByLine(5, 'left');
assert.equal(actual.textContent, diff.content[2].ab[0]);
actual = builder.getContentByLine(5, 'right');
assert.equal(actual.textContent, diff.content[1].b[0]);
});
test('findLinesByRange', function() {
var lines = [];
var elems = [];
var start = 6;
var end = 10;
var count = end - start + 1;
builder.findLinesByRange(start, end, 'right', lines, elems);
assert.equal(lines.length, count);
assert.equal(elems.length, count);
for (var i = 0; i < 5; i++) {
assert.instanceOf(lines[i], GrDiffLine);
assert.equal(lines[i].afterNumber, start + i);
assert.instanceOf(elems[i], HTMLElement);
assert.equal(lines[i].text, elems[i].textContent);
}
});
test('_renderContentByRange', function() {
var spy = sinon.spy(builder, '_createTextEl');
var start = 9;
var end = 14;
var count = end - start + 1;
builder._renderContentByRange(start, end, 'left');
assert.equal(spy.callCount, count);
spy.getCalls().forEach(function(call, i) {
assert.equal(call.args[0].beforeNumber, start + i);
});
spy.restore();
});
test('_getNextContentOnSide side-by-side left', function() {
var startElem = builder.getContentByLine(5, 'left',
element.$.diffTable);
var expectedStartString = diff.content[2].ab[0];
var expectedNextString = diff.content[2].ab[1];
assert.equal(startElem.textContent, expectedStartString);
var nextElem = builder._getNextContentOnSide(startElem,
'left');
assert.equal(nextElem.textContent, expectedNextString);
});
test('_getNextContentOnSide side-by-side right', function() {
var startElem = builder.getContentByLine(5, 'right',
element.$.diffTable);
var expectedStartString = diff.content[1].b[0];
var expectedNextString = diff.content[1].b[1];
assert.equal(startElem.textContent, expectedStartString);
var nextElem = builder._getNextContentOnSide(startElem,
'right');
assert.equal(nextElem.textContent, expectedNextString);
});
test('_getNextContentOnSide unified left', function(done) {
// Re-render as unified:
element.viewMode = 'UNIFIED_DIFF';
element.render({left: [], right: []}, prefs).then(function() {
builder = element._builder;
var startElem = builder.getContentByLine(5, 'left',
element.$.diffTable);
var expectedStartString = diff.content[2].ab[0];
var expectedNextString = diff.content[2].ab[1];
assert.equal(startElem.textContent, expectedStartString);
var nextElem = builder._getNextContentOnSide(startElem,
'left');
assert.equal(nextElem.textContent, expectedNextString);
done();
});
});
test('_getNextContentOnSide unified right', function(done) {
// Re-render as unified:
element.viewMode = 'UNIFIED_DIFF';
element.render({left: [], right: []}, prefs).then(function() {
builder = element._builder;
var startElem = builder.getContentByLine(5, 'right',
element.$.diffTable);
var expectedStartString = diff.content[1].b[0];
var expectedNextString = diff.content[1].b[1];
assert.equal(startElem.textContent, expectedStartString);
var nextElem = builder._getNextContentOnSide(startElem,
'right');
assert.equal(nextElem.textContent, expectedNextString);
done();
});
});
});
});
</script>