Nick Carter 8b4b718d54 GrDiffBuilder: performance optimizations for building diff rows
This shows a nearly 4x improvement (in Chrome) for building
the DOM structure for a large diff. (this doesn't include
style calculation and layout, which is still expensive).

Bug: Issue 6514
Change-Id: Id06db0c9a281b846078aef0e6d584e1fa3c03d0f
2017-11-20 21:33:00 +00:00

1092 lines
37 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-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../../test/common-test-setup.html"/>
<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">
<script>void(0);</script>
<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', () => {
let element;
let builder;
let sandbox;
const LINE_FEED_HTML = '<span class="style-scope gr-diff br"></span>';
setup(() => {
sandbox = sinon.sandbox.create();
stub('gr-rest-api-interface', {
getLoggedIn() { return Promise.resolve(false); },
getProjectConfig() { return Promise.resolve({}); },
});
const prefs = {
line_length: 10,
show_tabs: true,
tab_size: 4,
};
const projectName = 'my-project';
builder = new GrDiffBuilder(
{content: []}, {left: [], right: []}, prefs, projectName);
});
teardown(() => { sandbox.restore(); });
test('context control buttons', () => {
const section = {};
const line = {contextGroup: {lines: []}};
// Create 10 lines.
for (let i = 0; i < 10; i++) {
line.contextGroup.lines.push('lorem upsum');
}
// Does not include +10 buttons when there are fewer than 11 lines.
let td = builder._createContextControl(section, line);
let 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 1', () => {
let text = 'abcdef';
assert.equal(builder._formatText(text, 4, 10).innerHTML, text);
text = 'a'.repeat(20);
assert.equal(builder._formatText(text, 4, 10).innerHTML,
'a'.repeat(10) +
LINE_FEED_HTML +
'a'.repeat(10));
});
test('newlines 2', () => {
const text = '<span class="thumbsup">👍</span>';
assert.equal(builder._formatText(text, 4, 10).innerHTML,
'&lt;span clas' +
LINE_FEED_HTML +
's="thumbsu' +
LINE_FEED_HTML +
'p"&gt;👍&lt;/span' +
LINE_FEED_HTML +
'&gt;');
});
test('newlines 3', () => {
const text = '01234\t56789';
assert.equal(builder._formatText(text, 4, 10).innerHTML,
'01234' + builder._getTabWrapper(3).outerHTML + '56' +
LINE_FEED_HTML +
'789');
});
test('newlines 4', () => {
const text = '👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍';
assert.equal(builder._formatText(text, 4, 20).innerHTML,
'👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍' +
LINE_FEED_HTML +
'👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍' +
LINE_FEED_HTML +
'👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍');
});
test('line_length ignored if line_wrapping is true', () => {
builder._prefs = {line_wrapping: true, tab_size: 4, line_length: 50};
const text = 'a'.repeat(51);
const line = {text, highlights: []};
const result = builder._createTextEl(line).firstChild.innerHTML;
assert.equal(result, text);
});
test('line_length applied if line_wrapping is false', () => {
builder._prefs = {line_wrapping: false, tab_size: 4, line_length: 50};
const text = 'a'.repeat(51);
const line = {text, highlights: []};
const expected = 'a'.repeat(50) + LINE_FEED_HTML + 'a';
const result = builder._createTextEl(line).firstChild.innerHTML;
assert.equal(result, expected);
});
test('_createTextEl linewrap with tabs', () => {
const text = '\t'.repeat(7) + '!';
const line = {text, highlights: []};
const el = builder._createTextEl(line);
assert.equal(el.innerText, text);
// With line length 10 and tab size 2, there should be a line break
// after every two tabs.
const newlineEl = el.querySelector('.contentText > .br');
assert.isOk(newlineEl);
assert.equal(
el.querySelector('.contentText .tab:nth-child(2)').nextSibling,
newlineEl);
});
test('text length with tabs and unicode', () => {
function expectTextLength(text, tabSize, expected) {
// Formatting to |expected| columns should not introduce line breaks.
const result = builder._formatText(text, tabSize, expected);
assert.isNotOk(result.querySelector('.contentText > .br'),
` Expected the result of: \n` +
` _formatText(${text}', ${tabSize}, ${expected})\n` +
` to not contain a br. But the actual result HTML was:\n` +
` '${result.innerHTML}'\nwhereupon`);
// Increasing the line limit should produce the same markup.
assert.equal(builder._formatText(text, tabSize, Infinity).innerHTML,
result.innerHTML);
assert.equal(builder._formatText(text, tabSize, expected + 1).innerHTML,
result.innerHTML);
// Decreasing the line limit should introduce line breaks.
if (expected > 0) {
const tooSmall = builder._formatText(text, tabSize, expected - 1);
assert.isOk(tooSmall.querySelector('.contentText > .br'),
` Expected the result of: \n` +
` _formatText(${text}', ${tabSize}, ${expected - 1})\n` +
` to contain a br. But the actual result HTML was:\n` +
` '${tooSmall.innerHTML}'\nwhereupon`);
}
}
expectTextLength('12345', 4, 5);
expectTextLength('\t\t12', 4, 10);
expectTextLength('abc💢123', 4, 7);
expectTextLength('abc\t', 8, 8);
expectTextLength('abc\t\t', 10, 20);
expectTextLength('', 10, 0);
expectTextLength('', 10, 0);
// 17 Thai combining chars.
expectTextLength('ก้้้้้้้้้้้้้้้้', 4, 17);
expectTextLength('abc\tde', 10, 12);
expectTextLength('abc\tde\t', 10, 20);
expectTextLength('\t\t\t\t\t', 20, 100);
});
test('tab wrapper insertion', () => {
const html = 'abc\tdef';
const tabSize = builder._prefs.tab_size;
const wrapper = builder._getTabWrapper(tabSize - 3);
assert.ok(wrapper);
assert.equal(wrapper.innerText, '\t');
assert.equal(
builder._formatText(html, tabSize, Infinity).innerHTML,
'abc' + wrapper.outerHTML + 'def');
});
test('tab wrapper style', () => {
const pattern = new RegExp('^<span class="style-scope gr-diff tab" '
+ 'style="(?:-moz-)?tab-size: (\\d+);">\\t<\\/span>$');
for (const size of [1, 3, 8, 55]) {
const html = builder._getTabWrapper(size).outerHTML;
expect(html).to.match(pattern);
assert.equal(html.match(pattern)[1], size);
}
});
test('comments', () => {
const line = new GrDiffLine(GrDiffLine.Type.BOTH);
line.beforeNumber = 3;
line.afterNumber = 5;
let 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, __commentSide: 'left'},
{id: 'r5', line: 5, __commentSide: 'right'}]);
assert.deepEqual(builder._getCommentsForLine(comments, line,
GrDiffBuilder.Side.LEFT), [{id: 'l3', line: 3,
__commentSide: 'left'}]);
assert.deepEqual(builder._getCommentsForLine(comments, line,
GrDiffBuilder.Side.RIGHT), [{id: 'r5', line: 5,
__commentSide: 'right'}]);
});
test('comment thread group creation', () => {
const l3 = {id: 'l3', line: 3, updated: '2016-08-09 00:42:32.000000000',
__commentSide: 'left'};
const l5 = {id: 'l5', line: 5, updated: '2016-08-09 00:42:32.000000000',
__commentSide: 'left'};
const r5 = {id: 'r5', line: 5, updated: '2016-08-09 00:42:32.000000000',
__commentSide: 'right'};
builder._comments = {
meta: {
changeNum: '42',
patchRange: {
basePatchNum: 'PARENT',
patchNum: '3',
},
path: '/path/to/foo',
projectConfig: {foo: 'bar'},
},
left: [l3, l5],
right: [r5],
};
function checkThreadGroupProps(threadGroupEl, patchNum, isOnParent,
comments) {
assert.equal(threadGroupEl.changeNum, '42');
assert.equal(threadGroupEl.patchForNewThreads, patchNum);
assert.equal(threadGroupEl.path, '/path/to/foo');
assert.equal(threadGroupEl.isOnParent, isOnParent);
assert.deepEqual(threadGroupEl.projectName, 'my-project');
assert.deepEqual(threadGroupEl.comments, comments);
}
let line = new GrDiffLine(GrDiffLine.Type.BOTH);
line.beforeNumber = 5;
line.afterNumber = 5;
let threadGroupEl = builder._commentThreadGroupForLine(line);
checkThreadGroupProps(threadGroupEl, '3', false, [l5, r5]);
threadGroupEl =
builder._commentThreadGroupForLine(line, GrDiffBuilder.Side.RIGHT);
checkThreadGroupProps(threadGroupEl, '3', false, [r5]);
threadGroupEl =
builder._commentThreadGroupForLine(line, GrDiffBuilder.Side.LEFT);
checkThreadGroupProps(threadGroupEl, '3', true, [l5]);
builder._comments.meta.patchRange.basePatchNum = '1';
threadGroupEl = builder._commentThreadGroupForLine(line);
checkThreadGroupProps(threadGroupEl, '3', false, [l5, r5]);
threadEl =
builder._commentThreadGroupForLine(line, GrDiffBuilder.Side.LEFT);
checkThreadGroupProps(threadEl, '1', false, [l5]);
threadGroupEl =
builder._commentThreadGroupForLine(line, GrDiffBuilder.Side.RIGHT);
checkThreadGroupProps(threadGroupEl, '3', false, [r5]);
builder._comments.meta.patchRange.basePatchNum = 'PARENT';
line = new GrDiffLine(GrDiffLine.Type.REMOVE);
line.beforeNumber = 5;
line.afterNumber = 5;
threadGroupEl = builder._commentThreadGroupForLine(line);
checkThreadGroupProps(threadGroupEl, '3', true, [l5, r5]);
line = new GrDiffLine(GrDiffLine.Type.ADD);
line.beforeNumber = 3;
line.afterNumber = 5;
threadGroupEl = builder._commentThreadGroupForLine(line);
checkThreadGroupProps(threadGroupEl, '3', false, [l3, r5]);
});
suite('_isTotal', () => {
test('is total for add', () => {
const group = new GrDiffGroup(GrDiffGroup.Type.DELTA);
for (let idx = 0; idx < 10; idx++) {
group.addLine(new GrDiffLine(GrDiffLine.Type.ADD));
}
assert.isTrue(GrDiffBuilder.prototype._isTotal(group));
});
test('is total for remove', () => {
const group = new GrDiffGroup(GrDiffGroup.Type.DELTA);
for (let idx = 0; idx < 10; idx++) {
group.addLine(new GrDiffLine(GrDiffLine.Type.REMOVE));
}
assert.isTrue(GrDiffBuilder.prototype._isTotal(group));
});
test('not total for empty', () => {
const group = new GrDiffGroup(GrDiffGroup.Type.BOTH);
assert.isFalse(GrDiffBuilder.prototype._isTotal(group));
});
test('not total for non-delta', () => {
const group = new GrDiffGroup(GrDiffGroup.Type.DELTA);
for (let idx = 0; idx < 10; idx++) {
group.addLine(new GrDiffLine(GrDiffLine.Type.BOTH));
}
assert.isFalse(GrDiffBuilder.prototype._isTotal(group));
});
});
suite('intraline differences', () => {
let el;
let str;
let annotateElementSpy;
let layer;
function slice(str, start, end) {
return Array.from(str).slice(start, end).join('');
}
setup(() => {
el = fixture('div-with-text');
str = el.textContent;
annotateElementSpy = sandbox.spy(GrAnnotation, 'annotateElement');
layer = document.createElement('gr-diff-builder')
._createIntralineLayer();
});
test('annotate no highlights', () => {
const 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', () => {
const line = {
text: str,
highlights: [
{startIndex: 6, endIndex: 12},
{startIndex: 18, endIndex: 22},
],
};
const str0 = slice(str, 0, 6);
const str1 = slice(str, 6, 12);
const str2 = slice(str, 12, 18);
const str3 = slice(str, 18, 22);
const 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', () => {
const line = {
text: str,
highlights: [
{startIndex: 28},
],
};
const str0 = slice(str, 0, 28);
const 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', () => {
const 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', () => {
// Put some unicode into the string:
str = str.replace(/\s/g, '💢');
el.textContent = str;
const line = {
text: str,
highlights: [
{startIndex: 6, endIndex: 12},
],
};
const str0 = slice(str, 0, 6);
const str1 = slice(str, 6, 12);
const 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', () => {
// Put some unicode into the string:
str = str.replace(/\s/g, '💢');
el.textContent = str;
const line = {
text: str,
highlights: [
{startIndex: 6},
],
};
const str0 = slice(str, 0, 6);
const 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', () => {
let element;
let layer;
setup(() => {
element = fixture('basic');
element._showTabs = true;
layer = element._createTabIndicatorLayer();
});
test('does nothing with empty line', () => {
const line = {text: ''};
const el = document.createElement('div');
const annotateElementStub =
sandbox.stub(GrAnnotation, 'annotateElement');
layer.annotate(el, line);
assert.isFalse(annotateElementStub.called);
});
test('does nothing with no tabs', () => {
const str = 'lorem ipsum no tabs';
const line = {text: str};
const el = document.createElement('div');
el.textContent = str;
const annotateElementStub =
sandbox.stub(GrAnnotation, 'annotateElement');
layer.annotate(el, line);
assert.isFalse(annotateElementStub.called);
});
test('annotates tab at beginning', () => {
const str = '\tlorem upsum';
const line = {text: str};
const el = document.createElement('div');
el.textContent = str;
const annotateElementStub =
sandbox.stub(GrAnnotation, 'annotateElement');
layer.annotate(el, line);
assert.equal(annotateElementStub.callCount, 1);
const 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', () => {
element._showTabs = false;
const str = '\tlorem upsum';
const line = {text: str};
const el = document.createElement('div');
el.textContent = str;
const annotateElementStub =
sandbox.stub(GrAnnotation, 'annotateElement');
layer.annotate(el, line);
assert.isFalse(annotateElementStub.called);
});
test('annotates multiple in beginning', () => {
const str = '\t\tlorem upsum';
const line = {text: str};
const el = document.createElement('div');
el.textContent = str;
const annotateElementStub =
sandbox.stub(GrAnnotation, 'annotateElement');
layer.annotate(el, line);
assert.equal(annotateElementStub.callCount, 2);
let 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', () => {
const str = 'lorem\tupsum';
const line = {text: str};
const el = document.createElement('div');
el.textContent = str;
const annotateElementStub =
sandbox.stub(GrAnnotation, 'annotateElement');
layer.annotate(el, line);
assert.equal(annotateElementStub.callCount, 1);
const 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('trailing whitespace', () => {
let element;
let layer;
setup(() => {
element = fixture('basic');
element._showTrailingWhitespace = true;
layer = element._createTrailingWhitespaceLayer();
});
test('does nothing with empty line', () => {
const line = {text: ''};
const el = document.createElement('div');
const annotateElementStub =
sandbox.stub(GrAnnotation, 'annotateElement');
layer.annotate(el, line);
assert.isFalse(annotateElementStub.called);
});
test('does nothing with no trailing whitespace', () => {
const str = 'lorem ipsum blah blah';
const line = {text: str};
const el = document.createElement('div');
el.textContent = str;
const annotateElementStub =
sandbox.stub(GrAnnotation, 'annotateElement');
layer.annotate(el, line);
assert.isFalse(annotateElementStub.called);
});
test('annotates trailing spaces', () => {
const str = 'lorem ipsum ';
const line = {text: str};
const el = document.createElement('div');
el.textContent = str;
const annotateElementStub =
sandbox.stub(GrAnnotation, 'annotateElement');
layer.annotate(el, line);
assert.isTrue(annotateElementStub.called);
assert.equal(annotateElementStub.lastCall.args[1], 11);
assert.equal(annotateElementStub.lastCall.args[2], 3);
});
test('annotates trailing tabs', () => {
const str = 'lorem ipsum\t\t\t';
const line = {text: str};
const el = document.createElement('div');
el.textContent = str;
const annotateElementStub =
sandbox.stub(GrAnnotation, 'annotateElement');
layer.annotate(el, line);
assert.isTrue(annotateElementStub.called);
assert.equal(annotateElementStub.lastCall.args[1], 11);
assert.equal(annotateElementStub.lastCall.args[2], 3);
});
test('annotates mixed trailing whitespace', () => {
const str = 'lorem ipsum\t \t';
const line = {text: str};
const el = document.createElement('div');
el.textContent = str;
const annotateElementStub =
sandbox.stub(GrAnnotation, 'annotateElement');
layer.annotate(el, line);
assert.isTrue(annotateElementStub.called);
assert.equal(annotateElementStub.lastCall.args[1], 11);
assert.equal(annotateElementStub.lastCall.args[2], 3);
});
test('unicode preceding trailing whitespace', () => {
const str = '💢\t';
const line = {text: str};
const el = document.createElement('div');
el.textContent = str;
const annotateElementStub =
sandbox.stub(GrAnnotation, 'annotateElement');
layer.annotate(el, line);
assert.isTrue(annotateElementStub.called);
assert.equal(annotateElementStub.lastCall.args[1], 1);
assert.equal(annotateElementStub.lastCall.args[2], 1);
});
test('does not annotate when disabled', () => {
element._showTrailingWhitespace = false;
const str = 'lorem upsum\t \t ';
const line = {text: str};
const el = document.createElement('div');
el.textContent = str;
const annotateElementStub =
sandbox.stub(GrAnnotation, 'annotateElement');
layer.annotate(el, line);
assert.isFalse(annotateElementStub.called);
});
});
suite('rendering', () => {
let content;
let outputEl;
setup(done => {
const prefs = {
line_length: 10,
show_tabs: true,
tab_size: 4,
context: -1,
syntax_highlighting: true,
};
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: sandbox.stub(),
timeEnd: sandbox.stub(),
});
element = fixture('basic');
outputEl = element.queryEffectiveChildren('#diffTable');
sandbox.stub(element, '_getDiffBuilder', () => {
const builder = new GrDiffBuilder(
{content}, {left: [], right: []}, prefs, 'my-project', outputEl);
sandbox.stub(builder, 'addColumns');
builder.buildSectionElement = function(group) {
const section = document.createElement('stub');
section.textContent = group.lines.reduce((acc, line) => {
return acc + line.text;
}, '');
return section;
};
return builder;
});
element.diff = {content};
element.render({left: [], right: []}, prefs).then(done);
});
test('reporting', done => {
const timeStub = element.$.reporting.time;
const timeEndStub = element.$.reporting.timeEnd;
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', () => {
let section = outputEl.querySelector('stub:nth-of-type(2)');
const prevInnerHTML = section.innerHTML;
section.innerHTML = 'wiped';
element._builder.renderSection(section);
section = outputEl.querySelector('stub:nth-of-type(2)');
assert.equal(section.innerHTML, prevInnerHTML);
});
test('addColumns is called', done => {
element.render({left: [], right: []}, {}).then(done);
assert.isTrue(element._builder.addColumns.called);
});
test('getSectionsByLineRange one line', () => {
const section = outputEl.querySelector('stub:nth-of-type(2)');
const sections = element._builder.getSectionsByLineRange(1, 1, 'left');
assert.equal(sections.length, 1);
assert.strictEqual(sections[0], section);
});
test('getSectionsByLineRange over diff', () => {
const section = [
outputEl.querySelector('stub:nth-of-type(2)'),
outputEl.querySelector('stub:nth-of-type(3)'),
];
const 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', done => {
const dispatchEventStub = sandbox.stub(element, 'dispatchEvent');
element.render({left: [], right: []}, {}).then(() => {
const firedEventTypes = dispatchEventStub.getCalls()
.map(c => { return c.args[0].type; });
assert.include(firedEventTypes, 'render-start');
assert.include(firedEventTypes, 'render-content');
assert.include(firedEventTypes, 'render');
done();
});
});
test('rendering normal-sized diff does not disable syntax', () => {
assert.isTrue(element.$.syntaxLayer.enabled);
});
test('rendering large diff disables syntax', done => {
// Before it renders, set the first diff line to 500 '*' characters.
element.diff.content[0].a = [new Array(501).join('*')];
element.addEventListener('render', () => {
assert.isFalse(element.$.syntaxLayer.enabled);
done();
});
const prefs = {
line_length: 10,
show_tabs: true,
tab_size: 4,
context: -1,
syntax_highlighting: true,
};
element.render({left: [], right: []}, prefs);
});
test('cancel', () => {
const processorCancelStub = sandbox.stub(element.$.processor, 'cancel');
const syntaxCancelStub = sandbox.stub(element.$.syntaxLayer, 'cancel');
element.cancel();
assert.isTrue(processorCancelStub.called);
assert.isTrue(syntaxCancelStub.called);
});
});
suite('mock-diff', () => {
let element;
let builder;
let diff;
let prefs;
setup(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(() => {
builder = element._builder;
done();
});
});
test('getContentByLine', () => {
let 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', () => {
const lines = [];
const elems = [];
const start = 6;
const end = 10;
const count = end - start + 1;
builder.findLinesByRange(start, end, 'right', lines, elems);
assert.equal(lines.length, count);
assert.equal(elems.length, count);
for (let 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', () => {
const spy = sandbox.spy(builder, '_createTextEl');
const start = 9;
const end = 14;
const count = end - start + 1;
builder._renderContentByRange(start, end, 'left');
assert.equal(spy.callCount, count);
spy.getCalls().forEach((call, i) => {
assert.equal(call.args[0].beforeNumber, start + i);
});
});
test('_getNextContentOnSide side-by-side left', () => {
const startElem = builder.getContentByLine(5, 'left',
element.$.diffTable);
const expectedStartString = diff.content[2].ab[0];
const expectedNextString = diff.content[2].ab[1];
assert.equal(startElem.textContent, expectedStartString);
const nextElem = builder._getNextContentOnSide(startElem,
'left');
assert.equal(nextElem.textContent, expectedNextString);
});
test('_getNextContentOnSide side-by-side right', () => {
const startElem = builder.getContentByLine(5, 'right',
element.$.diffTable);
const expectedStartString = diff.content[1].b[0];
const expectedNextString = diff.content[1].b[1];
assert.equal(startElem.textContent, expectedStartString);
const nextElem = builder._getNextContentOnSide(startElem,
'right');
assert.equal(nextElem.textContent, expectedNextString);
});
test('_getNextContentOnSide unified left', done => {
// Re-render as unified:
element.viewMode = 'UNIFIED_DIFF';
element.render({left: [], right: []}, prefs).then(() => {
builder = element._builder;
const startElem = builder.getContentByLine(5, 'left',
element.$.diffTable);
const expectedStartString = diff.content[2].ab[0];
const expectedNextString = diff.content[2].ab[1];
assert.equal(startElem.textContent, expectedStartString);
const nextElem = builder._getNextContentOnSide(startElem,
'left');
assert.equal(nextElem.textContent, expectedNextString);
done();
});
});
test('_getNextContentOnSide unified right', done => {
// Re-render as unified:
element.viewMode = 'UNIFIED_DIFF';
element.render({left: [], right: []}, prefs).then(() => {
builder = element._builder;
const startElem = builder.getContentByLine(5, 'right',
element.$.diffTable);
const expectedStartString = diff.content[1].b[0];
const expectedNextString = diff.content[1].b[1];
assert.equal(startElem.textContent, expectedStartString);
const nextElem = builder._getNextContentOnSide(startElem,
'right');
assert.equal(nextElem.textContent, expectedNextString);
done();
});
});
test('escaping HTML', () => {
let input = '<script>alert("XSS");<' + '/script>';
let expected = '&lt;script&gt;alert("XSS");&lt;/script&gt;';
let result = builder._formatText(input, 1, Infinity).innerHTML;
assert.equal(result, expected);
input = '& < > " \' / `';
expected = '&amp; &lt; &gt; " \' / `';
result = builder._formatText(input, 1, Infinity).innerHTML;
assert.equal(result, expected);
});
});
suite('blame', () => {
let mockBlame;
setup(() => {
mockBlame = [
{id: 'commit 1', ranges: [{start: 1, end: 2}, {start: 10, end: 16}]},
{id: 'commit 2', ranges: [{start: 4, end: 10}, {start: 17, end: 32}]},
];
});
test('setBlame attempts to render each blamed line', () => {
const getBlameStub = sandbox.stub(builder, '_getBlameByLineNum')
.returns(null);
builder.setBlame(mockBlame);
assert.equal(getBlameStub.callCount, 32);
});
test('_getBlameCommitForBaseLine', () => {
builder.setBlame(mockBlame);
assert.isOk(builder._getBlameCommitForBaseLine(1));
assert.equal(builder._getBlameCommitForBaseLine(1).id, 'commit 1');
assert.isOk(builder._getBlameCommitForBaseLine(11));
assert.equal(builder._getBlameCommitForBaseLine(11).id, 'commit 1');
assert.isOk(builder._getBlameCommitForBaseLine(32));
assert.equal(builder._getBlameCommitForBaseLine(32).id, 'commit 2');
assert.isNull(builder._getBlameCommitForBaseLine(33));
});
test('_getBlameCommitForBaseLine w/o blame returns null', () => {
assert.isNull(builder._getBlameCommitForBaseLine(1));
assert.isNull(builder._getBlameCommitForBaseLine(11));
assert.isNull(builder._getBlameCommitForBaseLine(31));
});
test('_createBlameCell', () => {
const mocbBlameCell = document.createElement('span');
const getBlameStub = sinon.stub(builder, '_getBlameForBaseLine')
.returns(mocbBlameCell);
const line = new GrDiffLine(GrDiffLine.Type.BOTH);
line.beforeNumber = 3;
line.afterNumber = 5;
const result = builder._createBlameCell(line);
assert.isTrue(getBlameStub.calledWithExactly(3));
assert.equal(result.getAttribute('data-line-number'), '3');
assert.equal(result.firstChild, mocbBlameCell);
});
});
});
</script>