Files
gerrit/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
Ole Rehmsen e5c377cf7a Move hover handling out of gr-diff-comment
The only code that is interested in hover events from gr-comment-diff is
in gr-diff-highlight, which is used only by gr-diff. For these reasons,
it makes more sense for gr-diff (or gr-diff-highlight) to subscribe to
the native hover events of the comment threads associated with it, and
not require gr-comment to expose any custom events for that. This also
has the benefit that it reduces the requirements on thread elements:
They can just fire regular mouseenter and mouseleave events like any
DOMElement would. This makes it easier to use gr-diff with different
comment thread element implementations.

I used this change to also make the unit tests for this more meaningful
and not stub out all the interesting parts.

Change-Id: I1a969bb6a7092bc433039662cd2034dd7141e4ca
2018-11-19 16:14:38 +01:00

645 lines
24 KiB
HTML

<!DOCTYPE html>
<!--
@license
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-highlight</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"/>
<link rel="import" href="gr-diff-highlight.html">
<script>void(0);</script>
<test-fixture id="basic">
<template>
<style>
.tab-indicator:before {
color: #C62828;
/* >> character */
content: '\00BB';
}
</style>
<gr-diff-highlight>
<table id="diffTable">
<tbody class="section both">
<tr class="diff-row side-by-side" left-type="both" right-type="both">
<td class="left lineNum" data-value="1"></td>
<td class="content both"><div class="contentText">[1] Nam cum ad me in Cumanum salutandi causa uterque venisset,</div></td>
<td class="right lineNum" data-value="1"></td>
<td class="content both"><div class="contentText">[1] Nam cum ad me in Cumanum salutandi causa uterque</div></td>
</tr>
</tbody>
<tbody class="section delta">
<tr class="diff-row side-by-side" left-type="remove" right-type="add">
<td class="left lineNum" data-value="2"></td>
<!-- Next tag is formatted to eliminate zero-length text nodes. -->
<td class="content remove"><div class="contentText">na💢ti <hl class="foo">te, inquit</hl>, sumus <hl class="bar">aliquando</hl> otiosum, <hl>certe</hl> a <hl><span class="tab-indicator" style="tab-size:8;"> </span></hl>udiam, <hl>quid</hl> sit, <span class="tab-indicator" style="tab-size:8;"> </span>quod <hl>Epicurum</hl></div></td>
<td class="right lineNum" data-value="2"></td>
<!-- Next tag is formatted to eliminate zero-length text nodes. -->
<td class="content add"><div class="contentText">nacti , <hl>,</hl> sumus <hl><span class="tab-indicator" style="tab-size:8;"> </span></hl> otiosum, <span class="tab-indicator" style="tab-size:8;"> </span> audiam, sit, quod</div></td>
</tr>
</tbody>
<tbody class="section both">
<tr class="diff-row side-by-side" left-type="both" right-type="both">
<td class="left lineNum" data-value="138"></td>
<td class="content both"><div class="contentText">[14] Nam cum ad me in Cumanum salutandi causa uterque venisset,</div></td>
<td class="right lineNum" data-value="119"></td>
<td class="content both"><div class="contentText">[14] Nam cum ad me in Cumanum salutandi causa uterque venisset,</div></td>
</tr>
</tbody>
<tbody class="section delta">
<tr class="diff-row side-by-side" left-type="remove" right-type="add">
<td class="left lineNum" data-value="140"></td>
<!-- Next tag is formatted to eliminate zero-length text nodes. -->
<td class="content remove"><div class="contentText">na💢ti <hl class="foo">te, inquit</hl>, sumus <hl class="bar">aliquando</hl> otiosum, <hl>certe</hl> a <hl><span class="tab-indicator" style="tab-size:8;"> </span></hl>udiam, <hl>quid</hl> sit, <span class="tab-indicator" style="tab-size:8;"> </span>quod <hl>Epicurum</hl></div><gr-diff-comment-thread>
[Yet another random diff thread content here]
</gr-diff-comment-thread></td>
<td class="right lineNum" data-value="120"></td>
<!-- Next tag is formatted to eliminate zero-length text nodes. -->
<td class="content add"><div class="contentText">nacti , <hl>,</hl> sumus <hl><span class="tab-indicator" style="tab-size:8;"> </span></hl> otiosum, <span class="tab-indicator" style="tab-size:8;"> </span> audiam, sit, quod</div></td>
</tr>
</tbody>
<tbody class="section both">
<tr class="diff-row side-by-side" left-type="both" right-type="both">
<td class="left lineNum" data-value="141"></td>
<td class="content both"><div class="contentText">nam et<hl><span class="tab-indicator" style="tab-size:8;"> </span></hl>complectitur<span class="tab-indicator" style="tab-size:8;"> </span>verbis, quod vult, et dicit plane, quod intellegam;</div></td>
<td class="right lineNum" data-value="130"></td>
<td class="content both"><div class="contentText">nam et complectitur verbis, quod vult, et dicit plane, quod intellegam;</div></td>
</tr>
</tbody>
<tbody class="section contextControl">
<tr class="diff-row side-by-side" left-type="contextControl" right-type="contextControl">
<td class="left contextLineNum" data-value="@@"></td>
<td>
<gr-button>+10↑</gr-button>
-
<gr-button>Show 21 common lines</gr-button>
-
<gr-button>+10↓</gr-button>
</td>
<td class="right contextLineNum" data-value="@@"></td>
<td>
<gr-button>+10↑</gr-button>
-
<gr-button>Show 21 common lines</gr-button>
-
<gr-button>+10↓</gr-button>
</td>
</tr>
</tbody>
<tbody class="section delta total">
<tr class="diff-row side-by-side" left-type="blank" right-type="add">
<td class="left"></td>
<td class="blank"></td>
<td class="right lineNum" data-value="146"></td>
<td class="content add"><div class="contentText">[17] Quid igitur est? inquit; audire enim cupio, quid non probes. Principio, inquam,</div></td>
</tr>
</tbody>
<tbody class="section both">
<tr class="diff-row side-by-side" left-type="both" right-type="both">
<td class="left lineNum" data-value="165"></td>
<td class="content both"><div class="contentText"></div></td>
<td class="right lineNum" data-value="147"></td>
<td class="content both"><div class="contentText">in physicis, <hl><span class="tab-indicator" style="tab-size:8;"> </span></hl> quibus maxime gloriatur, primum totus est alienus. Democritea dicit</div></td>
</tr>
</tbody>
</table>
</gr-diff-highlight>
</template>
</test-fixture>
<test-fixture id="highlighted">
<template>
<div>
<hl class="rangeHighlight">foo</hl>
bar
<hl class="rangeHighlight">baz</hl>
</div>
</template>
</test-fixture>
<script>
suite('gr-diff-highlight', () => {
let element;
let sandbox;
setup(() => {
sandbox = sinon.sandbox.create();
element = fixture('basic')[1];
});
teardown(() => {
sandbox.restore();
});
suite('selectionchange event handling', () => {
const emulateSelection = function() {
document.dispatchEvent(new CustomEvent('selectionchange'));
element.flushDebouncer('selectionChange');
element.flushDebouncer('removeActionBox');
};
setup(() => {
sandbox.stub(element, '_handleSelection');
sandbox.stub(element, '_removeActionBox');
});
test('enabled if logged in', () => {
element.loggedIn = true;
emulateSelection();
assert.isTrue(element._handleSelection.called);
assert.isTrue(element._removeActionBox.called);
});
test('ignored if logged out', () => {
element.loggedIn = false;
emulateSelection();
assert.isFalse(element._handleSelection.called);
assert.isFalse(element._removeActionBox.called);
});
});
suite('comment events', () => {
let builder;
setup(() => {
builder = {
getContentsByLineRange: sandbox.stub().returns([]),
getLineElByChild: sandbox.stub().returns({}),
getSideByLineEl: sandbox.stub().returns('other-side'),
};
element._cachedDiffBuilder = builder;
});
test('comment-thread-mouseenter from line comments is ignored', () => {
const threadEl = document.createElement('div');
threadEl.setAttribute('comment-side', 'right');
threadEl.setAttribute('line-num', 3);
element.appendChild(threadEl);
element.commentRanges = [{side: 'right'}];
sandbox.stub(element, 'set');
threadEl.dispatchEvent(
new CustomEvent('comment-thread-mouseenter', {bubbles: true}));
assert.isFalse(element.set.called);
});
test('comment-thread-mouseenter from ranged comment causes set', () => {
const threadEl = document.createElement('div');
threadEl.setAttribute('comment-side', 'right');
threadEl.setAttribute('line-num', 3);
threadEl.setAttribute('range', JSON.stringify({
start_line: 3,
start_character: 4,
end_line: 5,
end_character: 6,
}));
element.appendChild(threadEl);
element.commentRanges = [{side: 'right', range: {
start_line: 3,
start_character: 4,
end_line: 5,
end_character: 6,
}}];
sandbox.stub(element, 'set');
threadEl.dispatchEvent(
new CustomEvent('comment-thread-mouseenter', {bubbles: true}));
assert.isTrue(element.set.called);
const args = element.set.lastCall.args;
assert.deepEqual(args[0], ['commentRanges', 0, 'hovering']);
assert.deepEqual(args[1], true);
});
test('comment-thread-mouseleave from line comments is ignored', () => {
const threadEl = document.createElement('div');
threadEl.setAttribute('comment-side', 'right');
threadEl.setAttribute('line-num', 3);
element.appendChild(threadEl);
element.commentRanges = [{side: 'right'}];
sandbox.stub(element, 'set');
threadEl.dispatchEvent(
new CustomEvent('comment-thread-mouseleave', {bubbles: true}));
assert.isFalse(element.set.called);
});
test('on create-range-comment action box is removed', () => {
sandbox.stub(element, '_removeActionBox');
element.fire('create-range-comment', {
comment: {
range: {},
},
});
assert.isTrue(element._removeActionBox.called);
});
});
suite('selection', () => {
let diff;
let builder;
let contentStubs;
const stubContent = (line, side, opt_child) => {
const contentTd = diff.querySelector(
`.${side}.lineNum[data-value="${line}"] ~ .content`);
const contentText = contentTd.querySelector('.contentText');
const lineEl = diff.querySelector(
`.${side}.lineNum[data-value="${line}"]`);
contentStubs.push({
lineEl,
contentTd,
contentText,
});
builder.getContentByLineEl.withArgs(lineEl).returns(contentText);
builder.getLineNumberByChild.withArgs(lineEl).returns(line);
builder.getContentByLine.withArgs(line, side).returns(contentText);
builder.getSideByLineEl.withArgs(lineEl).returns(side);
return contentText;
};
const emulateSelection = (startNode, startOffset, endNode, endOffset) => {
const selection = window.getSelection();
const range = document.createRange();
range.setStart(startNode, startOffset);
range.setEnd(endNode, endOffset);
selection.addRange(range);
element._handleSelection();
};
const getActionRange = () =>
Polymer.dom(element.root).querySelector(
'gr-selection-action-box').range;
const getActionSide = () =>
Polymer.dom(element.root).querySelector(
'gr-selection-action-box').side;
const getLineElByChild = node => {
const stubs = contentStubs.find(stub => stub.contentTd.contains(node));
return stubs && stubs.lineEl;
};
setup(() => {
contentStubs = [];
stub('gr-selection-action-box', {
placeAbove: sandbox.stub(),
placeBelow: sandbox.stub(),
});
diff = element.querySelector('#diffTable');
builder = {
getContentByLine: sandbox.stub(),
getContentByLineEl: sandbox.stub(),
getLineElByChild,
getLineNumberByChild: sandbox.stub(),
getSideByLineEl: sandbox.stub(),
};
element._cachedDiffBuilder = builder;
});
teardown(() => {
contentStubs = null;
window.getSelection().removeAllRanges();
});
test('single first line', () => {
const content = stubContent(1, 'right');
sandbox.spy(element, '_positionActionBox');
emulateSelection(content.firstChild, 5, content.firstChild, 12);
const actionBox = element.$$('gr-selection-action-box');
assert.isTrue(actionBox.positionBelow);
});
test('multiline starting on first line', () => {
const startContent = stubContent(1, 'right');
const endContent = stubContent(2, 'right');
sandbox.spy(element, '_positionActionBox');
emulateSelection(
startContent.firstChild, 10, endContent.lastChild, 7);
const actionBox = element.$$('gr-selection-action-box');
assert.isTrue(actionBox.positionBelow);
});
test('single line', () => {
const content = stubContent(138, 'left');
sandbox.spy(element, '_positionActionBox');
emulateSelection(content.firstChild, 5, content.firstChild, 12);
const actionBox = element.$$('gr-selection-action-box');
assert.isTrue(element.isRangeSelected());
assert.deepEqual(getActionRange(), {
start_line: 138,
start_character: 5,
end_line: 138,
end_character: 12,
});
assert.equal(getActionSide(), 'left');
assert.notOk(actionBox.positionBelow);
});
test('multiline', () => {
const startContent = stubContent(119, 'right');
const endContent = stubContent(120, 'right');
sandbox.spy(element, '_positionActionBox');
emulateSelection(
startContent.firstChild, 10, endContent.lastChild, 7);
assert.isTrue(element.isRangeSelected());
const actionBox = element.$$('gr-selection-action-box');
assert.deepEqual(getActionRange(), {
start_line: 119,
start_character: 10,
end_line: 120,
end_character: 36,
});
assert.equal(getActionSide(), 'right');
assert.notOk(actionBox.positionBelow);
});
test('multiple ranges aka firefox implementation', () => {
const startContent = stubContent(119, 'right');
const endContent = stubContent(120, 'right');
const startRange = document.createRange();
startRange.setStart(startContent.firstChild, 10);
startRange.setEnd(startContent.firstChild, 11);
const endRange = document.createRange();
endRange.setStart(endContent.lastChild, 6);
endRange.setEnd(endContent.lastChild, 7);
const getRangeAtStub = sandbox.stub();
getRangeAtStub
.onFirstCall().returns(startRange)
.onSecondCall().returns(endRange);
sandbox.stub(window, 'getSelection').returns({
rangeCount: 2,
getRangeAt: getRangeAtStub,
removeAllRanges: sandbox.stub(),
});
element._handleSelection();
assert.isTrue(element.isRangeSelected());
assert.deepEqual(getActionRange(), {
start_line: 119,
start_character: 10,
end_line: 120,
end_character: 36,
});
});
test('multiline grow end highlight over tabs', () => {
const startContent = stubContent(119, 'right');
const endContent = stubContent(120, 'right');
emulateSelection(startContent.firstChild, 10, endContent.firstChild, 2);
assert.isTrue(element.isRangeSelected());
assert.deepEqual(getActionRange(), {
start_line: 119,
start_character: 10,
end_line: 120,
end_character: 2,
});
assert.equal(getActionSide(), 'right');
});
test('collapsed', () => {
const content = stubContent(138, 'left');
emulateSelection(content.firstChild, 5, content.firstChild, 5);
assert.isOk(window.getSelection().getRangeAt(0).startContainer);
assert.isFalse(element.isRangeSelected());
});
test('starts inside hl', () => {
const content = stubContent(140, 'left');
const hl = content.querySelector('.foo');
emulateSelection(hl.firstChild, 2, hl.nextSibling, 7);
assert.isTrue(element.isRangeSelected());
assert.deepEqual(getActionRange(), {
start_line: 140,
start_character: 8,
end_line: 140,
end_character: 23,
});
assert.equal(getActionSide(), 'left');
});
test('ends inside hl', () => {
const content = stubContent(140, 'left');
const hl = content.querySelector('.bar');
emulateSelection(hl.previousSibling, 2, hl.firstChild, 3);
assert.isTrue(element.isRangeSelected());
assert.deepEqual(getActionRange(), {
start_line: 140,
start_character: 18,
end_line: 140,
end_character: 27,
});
});
test('multiple hl', () => {
const content = stubContent(140, 'left');
const hl = content.querySelectorAll('hl')[4];
emulateSelection(content.firstChild, 2, hl.firstChild, 2);
assert.isTrue(element.isRangeSelected());
assert.deepEqual(getActionRange(), {
start_line: 140,
start_character: 2,
end_line: 140,
end_character: 61,
});
assert.equal(getActionSide(), 'left');
});
test('starts outside of diff', () => {
const contentText = stubContent(140, 'left');
const contentTd = contentText.parentElement;
emulateSelection(contentTd.previousElementSibling, 0,
contentText.firstChild, 2);
assert.isFalse(element.isRangeSelected());
});
test('ends outside of diff', () => {
const content = stubContent(140, 'left');
emulateSelection(content.nextElementSibling.firstChild, 2,
content.firstChild, 2);
assert.isFalse(element.isRangeSelected());
});
test('starts and ends on different sides', () => {
const startContent = stubContent(140, 'left');
const endContent = stubContent(130, 'right');
emulateSelection(startContent.firstChild, 2, endContent.firstChild, 2);
assert.isFalse(element.isRangeSelected());
});
test('starts in comment thread element', () => {
const startContent = stubContent(140, 'left');
const comment = startContent.parentElement.querySelector(
'gr-diff-comment-thread');
const endContent = stubContent(141, 'left');
emulateSelection(comment.firstChild, 2, endContent.firstChild, 4);
assert.isTrue(element.isRangeSelected());
assert.deepEqual(getActionRange(), {
start_line: 140,
start_character: 83,
end_line: 141,
end_character: 4,
});
assert.equal(getActionSide(), 'left');
});
test('ends in comment thread element', () => {
const content = stubContent(140, 'left');
const comment = content.parentElement.querySelector(
'gr-diff-comment-thread');
emulateSelection(content.firstChild, 4, comment.firstChild, 1);
assert.isTrue(element.isRangeSelected());
assert.deepEqual(getActionRange(), {
start_line: 140,
start_character: 4,
end_line: 140,
end_character: 83,
});
assert.equal(getActionSide(), 'left');
});
test('starts in context element', () => {
const contextControl =
diff.querySelector('.contextControl').querySelector('gr-button');
const content = stubContent(146, 'right');
emulateSelection(contextControl, 0, content.firstChild, 7);
// TODO (viktard): Select nearest line.
assert.isFalse(element.isRangeSelected());
});
test('ends in context element', () => {
const contextControl =
diff.querySelector('.contextControl').querySelector('gr-button');
const content = stubContent(141, 'left');
emulateSelection(content.firstChild, 2, contextControl, 1);
// TODO (viktard): Select nearest line.
assert.isFalse(element.isRangeSelected());
});
test('selection containing context element', () => {
const startContent = stubContent(130, 'right');
const endContent = stubContent(146, 'right');
emulateSelection(startContent.firstChild, 3, endContent.firstChild, 14);
assert.isTrue(element.isRangeSelected());
assert.deepEqual(getActionRange(), {
start_line: 130,
start_character: 3,
end_line: 146,
end_character: 14,
});
assert.equal(getActionSide(), 'right');
});
test('ends at a tab', () => {
const content = stubContent(140, 'left');
emulateSelection(
content.firstChild, 1, content.querySelector('span'), 0);
assert.isTrue(element.isRangeSelected());
assert.deepEqual(getActionRange(), {
start_line: 140,
start_character: 1,
end_line: 140,
end_character: 51,
});
assert.equal(getActionSide(), 'left');
});
test('starts at a tab', () => {
const content = stubContent(140, 'left');
emulateSelection(
content.querySelectorAll('hl')[3], 0,
content.querySelectorAll('span')[1].nextSibling, 1);
assert.isTrue(element.isRangeSelected());
assert.deepEqual(getActionRange(), {
start_line: 140,
start_character: 51,
end_line: 140,
end_character: 71,
});
assert.equal(getActionSide(), 'left');
});
test('properly accounts for syntax highlighting', () => {
const content = stubContent(140, 'left');
const spy = sinon.spy(element, '_normalizeRange');
emulateSelection(
content.querySelectorAll('hl')[3], 0,
content.querySelectorAll('span')[1], 0);
const spyCall = spy.getCall(0);
const range = window.getSelection().getRangeAt(0);
assert.notDeepEqual(spyCall.returnValue, range);
});
test('GrRangeNormalizer._getTextOffset computes text offset', () => {
let content = stubContent(140, 'left');
let child = content.lastChild.lastChild;
let result = GrRangeNormalizer._getTextOffset(content, child);
assert.equal(result, 75);
content = stubContent(146, 'right');
child = content.lastChild;
result = GrRangeNormalizer._getTextOffset(content, child);
assert.equal(result, 0);
});
test('_fixTripleClickSelection', () => {
const startContent = stubContent(119, 'right');
const endContent = stubContent(120, 'right');
emulateSelection(startContent.firstChild, 0, endContent.firstChild, 0);
assert.isTrue(element.isRangeSelected());
assert.deepEqual(getActionRange(), {
start_line: 119,
start_character: 0,
end_line: 119,
end_character: element._getLength(startContent),
});
assert.equal(getActionSide(), 'right');
});
test('_fixTripleClickSelection empty line', () => {
const startContent = stubContent(146, 'right');
const endContent = stubContent(165, 'left');
emulateSelection(startContent.firstChild, 0,
endContent.parentElement.previousElementSibling, 0);
assert.isTrue(element.isRangeSelected());
assert.deepEqual(getActionRange(), {
start_line: 146,
start_character: 0,
end_line: 146,
end_character: 84,
});
assert.equal(getActionSide(), 'right');
});
});
});
</script>