2016-06-10 12:47:57 -07:00
<!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-highlight< / title >
2017-03-28 17:02:44 -07:00
< script src = "../../../bower_components/webcomponentsjs/webcomponents-lite.min.js" > < / script >
2016-06-10 12:47:57 -07:00
< script src = "../../../bower_components/web-component-tester/browser.js" > < / script >
Polygerrit now loads polymer-resin
polymer-resin intercepts polymer property assignments
before they reach XSS-vulnerable sinks like `href="..."`
and text nodes in `<script>` elements.
This follows the instructions in WORKSPACE for adding a new bower
dependency with kaspern's tweak to use the dependency in a rule so
that it's found. //lib/js/bower_components.bzl has already been
rolled-back per those instructions.
The license is the polymer license as can be seen at
https://github.com/Polymer/polymer-resin/blob/master/LICENSE though
I'm not sure that //tools/js/bower2bazel.py recognizes it as such.
Docs for the added component are available at
https://github.com/Polymer/polymer-resin/blob/master/README.md
https://github.com/Polymer/polymer-resin/blob/master/getting-started.md
With this change, when I introduce an XSS vulnerability as below,
polymer-resin intercepts and stops it.
Patch that introduces a strawman vulnerability.
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
@@ -55,6 +55,10 @@
url: '/q/status:abandoned',
name: 'Abandoned',
},
+ {
+ url: location.hash.replace(/^#/, '') || 'http://example.com/#fragment_echoed_here',
+ name: 'XSS Me',
+ },
],
}];
---
Address kaspern's and paladox's comments.
---
Undo version bumps for bower dependencies.
---
Change Soy index template to parallel app/index.html.
---
update polymer-resin to version 1.1.1-beta
----
Load polymer-resin into polygerrit-ui/**/*_test.html
After this, I ran the tests with
-l chrome
-l firefox
I ran a handful of tests with -p and observed that the
console shows "initResin" is called before test cases start
executing.
These changes were done programmaticly by running the script below
(approximately) thus:
```
gerrit/ $ cd polygerrit-ui/app
app/ $ find . -name \*test.html | xargs perl hack-tests.pl
```
```
use strict;
sub removeResin($) {
my $s = $_[0];
$s =~ s@<link rel="import" href="[^"]*/polymer-resin/[^"]*"[^>]*>\n?@@;
$s =~ s@<script src="[^"]*/polymer-resin/[^"]*"></script>\n?@@;
$s =~ s@<script>\s*security\.polymer_resin.*?</script>\n?@@s;
return $s;
}
for my $f (@ARGV) {
next if $f =~ m@/bower_components/|/node_modules/@;
system('git', 'checkout', $f);
print "$f\n";
my @lines = ();
open(IN, "<$f") or die "$f: $!";
my $maxLineOfMatch = 0;
while (<IN>) {
push(@lines, $_);
# Put a marker after core loading directives.
$maxLineOfMatch = scalar(@lines)
if m@/webcomponentsjs/|/polymer[.]html\b|/browser[.]js@;
}
close(IN) or die "$f: $!";
die "$f missing loading directives" unless $maxLineOfMatch;
# Given ./a/b/c/my_test.html, $pathToRoot is "../../.."
# assuming no non-leading . or .. components in the path from find.
my $pathToRoot = $f;
$pathToRoot =~ s@^\.\/@@;
$pathToRoot =~ s@^(.*?/)?app/@@;
$pathToRoot =~ s@\/[^\/]*$@@;
$pathToRoot =~ s@[^/]+@..@g;
my $nLines = scalar(@lines);
open(OUT, ">$f") or die "$f: $!";
# Output the lines up to the last polymer-resin dependency
# loaded explicitly by this test.
my $before = join '', @lines[0..($maxLineOfMatch - 1)];
$before = removeResin($before);
print OUT "$before";
# Dump out the lines that load polymer-resin and configure it for
# polygerrit.
if (1) {
print OUT qq'<link rel="import" href="$pathToRoot/bower_components/polymer-resin/standalone/polymer-resin-debug.html"/>
<script>
security.polymer_resin.install({allowedIdentifierPrefixes: [\'\']});
</script>
';
}
# Emit any remaining lines.
my $after = join '', @lines[$maxLineOfMatch..$#lines];
$after = removeResin($after);
$after =~ s/^\n*//;
print OUT "$after";
close(OUT) or die "$f: $!";
}
```
---
update polymer-resin to version 1.2.1-beta
---
update Soy index template to new style polymer-resin initialization
----
fix lint warnings
----
Load test/common-test-setup.html into *_test.html
Instead of inserting instructions to load and initialize polymer-resin into
every test file, add a common-test-setup.html that does that and also fold
iron-test-helpers loading into it.
----
imported files do not need to load webcomponentsjs
Change-Id: I71221c36ed8a0fe7f8720c1064a2fcc9555bb8df
2017-05-08 14:07:13 -04:00
< link rel = "import" href = "../../../test/common-test-setup.html" / >
2016-06-10 12:47:57 -07:00
< link rel = "import" href = "gr-diff-highlight.html" >
2017-03-28 17:02:44 -07:00
< script > void ( 0 ) ; < / script >
2016-06-10 12:47:57 -07:00
< test-fixture id = "basic" >
< template >
2017-02-06 10:47:53 -08:00
< style >
.tab-indicator:before {
color: #C62828;
/* >> character */
content: '\00BB';
}
< / style >
2016-06-22 10:32:00 -07:00
< gr-diff-highlight >
< table id = "diffTable" >
< tbody class = "section both" >
< tr class = "diff-row side-by-side" left-type = "both" right-type = "both" >
2017-02-06 10:47:53 -08:00
< td class = "left lineNum" data-value = "138" > < / td >
2016-12-21 12:55:21 -08:00
< td class = "content both" > < div class = "contentText" > [14] Nam cum ad me in Cumanum salutandi causa uterque venisset,< / div > < / td >
2017-02-06 10:47:53 -08:00
< td class = "right lineNum" data-value = "119" > < / td >
2016-12-21 12:55:21 -08:00
< td class = "content both" > < div class = "contentText" > [14] Nam cum ad me in Cumanum salutandi causa uterque venisset,< / div > < / td >
2016-06-22 10:32:00 -07:00
< / tr >
< / tbody >
< tbody class = "section delta" >
< tr class = "diff-row side-by-side" left-type = "remove" right-type = "add" >
2017-02-06 10:47:53 -08:00
< td class = "left lineNum" data-value = "140" > < / td >
2016-06-22 10:32:00 -07:00
<!-- Next tag is formatted to eliminate zero - length text nodes. -->
2017-02-06 10:47:53 -08:00
< 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 >
2016-06-22 10:32:00 -07:00
[Yet another random diff thread content here]
< / gr-diff-comment-thread > < / td >
2017-02-06 10:47:53 -08:00
< td class = "right lineNum" data-value = "120" > < / td >
2016-06-28 14:48:33 -07:00
<!-- Next tag is formatted to eliminate zero - length text nodes. -->
2017-02-06 10:47:53 -08:00
< 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 >
2016-06-22 10:32:00 -07:00
< / tr >
< / tbody >
< tbody class = "section both" >
< tr class = "diff-row side-by-side" left-type = "both" right-type = "both" >
2016-06-06 12:20:31 -07:00
< td class = "left lineNum" data-value = "141" > < / td >
2017-02-06 10:47:53 -08:00
< 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 >
2016-06-22 10:32:00 -07:00
< td class = "right lineNum" data-value = "130" > < / td >
2016-12-21 12:55:21 -08:00
< td class = "content both" > < div class = "contentText" > nam et complectitur verbis, quod vult, et dicit plane, quod intellegam;< / div > < / td >
2016-06-22 10:32:00 -07:00
< / tr >
< / tbody >
2016-06-06 12:20:31 -07:00
< 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 >
2016-12-21 12:55:21 -08:00
< tbody class = "section delta total" >
2016-06-06 12:20:31 -07:00
< tr class = "diff-row side-by-side" left-type = "blank" right-type = "add" >
< td class = "left" > < / td >
2016-12-21 12:55:21 -08:00
< td class = "blank" > < / td >
2016-06-06 12:20:31 -07:00
< td class = "right lineNum" data-value = "146" > < / td >
2016-12-21 12:55:21 -08:00
< td class = "content add" > < div class = "contentText" > [17] Quid igitur est? inquit; audire enim cupio, quid non probes. Principio, inquam,< / div > < / td >
2016-06-06 12:20:31 -07:00
< / 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 >
2016-12-21 12:55:21 -08:00
< td class = "content both" > < div class = "contentText" > in physicis, quibus maxime gloriatur, primum totus est alienus. Democritea dicit< / div > < / td >
2016-06-06 12:20:31 -07:00
< td class = "right lineNum" data-value = "147" > < / td >
2017-02-06 10:47:53 -08:00
< 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 >
2016-06-06 12:20:31 -07:00
< / tr >
< / tbody >
2016-06-22 10:32:00 -07:00
< / 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 >
2016-06-10 12:47:57 -07:00
< / template >
< / test-fixture >
< script >
2017-05-16 14:17:14 -07:00
suite('gr-diff-highlight', () => {
let element;
let sandbox;
2016-06-10 12:47:57 -07:00
2017-05-16 14:17:14 -07:00
setup(() => {
2016-06-22 10:32:00 -07:00
sandbox = sinon.sandbox.create();
2017-02-06 10:47:53 -08:00
element = fixture('basic')[1];
2016-06-10 12:47:57 -07:00
});
2017-05-16 14:17:14 -07:00
teardown(() => {
2016-06-22 10:32:00 -07:00
sandbox.restore();
});
2017-05-16 14:17:14 -07:00
suite('selectionchange event handling', () => {
const emulateSelection = function() {
2016-08-05 14:11:59 -07:00
document.dispatchEvent(new CustomEvent('selectionchange'));
element.flushDebouncer('selectionChange');
element.flushDebouncer('removeActionBox');
};
2017-05-16 14:17:14 -07:00
setup(() => {
2016-08-05 14:11:59 -07:00
sandbox.stub(element, '_handleSelection');
sandbox.stub(element, '_removeActionBox');
});
2017-05-16 14:17:14 -07:00
test('enabled if logged in', () => {
2016-08-05 14:11:59 -07:00
element.loggedIn = true;
emulateSelection();
assert.isTrue(element._handleSelection.called);
assert.isTrue(element._removeActionBox.called);
});
2017-05-16 14:17:14 -07:00
test('ignored if logged out', () => {
2016-08-05 14:11:59 -07:00
element.loggedIn = false;
emulateSelection();
assert.isFalse(element._handleSelection.called);
assert.isFalse(element._removeActionBox.called);
});
2016-06-06 12:20:31 -07:00
});
2017-05-16 14:17:14 -07:00
suite('comment events', () => {
let builder;
2016-06-17 16:53:45 -07:00
2017-05-16 14:17:14 -07:00
setup(() => {
2016-06-17 16:53:45 -07:00
builder = {
getContentsByLineRange: sandbox.stub().returns([]),
getLineElByChild: sandbox.stub().returns({}),
getSideByLineEl: sandbox.stub().returns('other-side'),
};
element._cachedDiffBuilder = builder;
});
2017-05-16 14:17:14 -07:00
test('comment-mouse-over from line comments is ignored', () => {
2016-07-14 12:31:09 -07:00
sandbox.stub(element, 'set');
2016-06-17 16:53:45 -07:00
element.fire('comment-mouse-over', {comment: {}});
2016-07-14 12:31:09 -07:00
assert.isFalse(element.set.called);
});
2017-05-16 14:17:14 -07:00
test('comment-mouse-over from ranged comment causes set', () => {
2016-07-14 12:31:09 -07:00
sandbox.stub(element, 'set');
sandbox.stub(element, '_indexOfComment').returns(0);
2016-06-22 17:18:06 -07:00
element.fire('comment-mouse-over', {comment: {range: {}}});
2016-07-14 12:31:09 -07:00
assert.isTrue(element.set.called);
2016-06-17 16:53:45 -07:00
});
2017-05-16 14:17:14 -07:00
test('comment-mouse-out from line comments is ignored', () => {
2016-06-17 16:53:45 -07:00
element.fire('comment-mouse-over', {comment: {}});
assert.isFalse(builder.getContentsByLineRange.called);
});
2017-05-16 14:17:14 -07:00
test('on create-comment action box is removed', () => {
2016-06-17 16:53:45 -07:00
sandbox.stub(element, '_removeActionBox');
element.fire('create-comment', {
comment: {
range: {},
},
});
assert.isTrue(element._removeActionBox.called);
});
2016-06-10 12:47:57 -07:00
});
2016-06-22 10:32:00 -07:00
2017-05-16 14:17:14 -07:00
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}"]`);
2016-06-06 12:20:31 -07:00
contentStubs.push({
2017-05-16 14:17:14 -07:00
lineEl,
contentTd,
contentText,
2016-06-06 12:20:31 -07:00
});
2016-07-01 09:30:44 -07:00
builder.getContentByLineEl.withArgs(lineEl).returns(contentText);
2016-06-06 12:20:31 -07:00
builder.getLineNumberByChild.withArgs(lineEl).returns(line);
2016-07-01 09:30:44 -07:00
builder.getContentByLine.withArgs(line, side).returns(contentText);
2016-06-06 12:20:31 -07:00
builder.getSideByLineEl.withArgs(lineEl).returns(side);
2016-07-01 09:30:44 -07:00
return contentText;
2016-06-06 12:20:31 -07:00
};
2017-05-16 14:17:14 -07:00
const emulateSelection = (startNode, startOffset, endNode, endOffset) => {
const selection = window.getSelection();
const range = document.createRange();
2016-06-06 12:20:31 -07:00
range.setStart(startNode, startOffset);
range.setEnd(endNode, endOffset);
selection.addRange(range);
element._handleSelection();
};
2017-05-16 14:17:14 -07:00
const getActionRange = () =>
Polymer.dom(element.root).querySelector(
'gr-selection-action-box').range;
2016-06-06 12:20:31 -07:00
2017-05-16 14:17:14 -07:00
const getActionSide = () =>
Polymer.dom(element.root).querySelector(
'gr-selection-action-box').side;
2016-06-06 12:20:31 -07:00
2017-05-16 14:17:14 -07:00
const getLineElByChild = node => {
const stubs = contentStubs.find(stub => stub.contentTd.contains(node));
2016-06-06 12:20:31 -07:00
return stubs & & stubs.lineEl;
};
2017-05-16 14:17:14 -07:00
setup(() => {
2016-06-06 12:20:31 -07:00
contentStubs = [];
stub('gr-selection-action-box', {
placeAbove: sandbox.stub(),
});
diff = element.querySelector('#diffTable');
builder = {
getContentByLine: sandbox.stub(),
getContentByLineEl: sandbox.stub(),
2017-05-16 14:17:14 -07:00
getLineElByChild,
2016-06-06 12:20:31 -07:00
getLineNumberByChild: sandbox.stub(),
getSideByLineEl: sandbox.stub(),
};
element._cachedDiffBuilder = builder;
});
2017-05-16 14:17:14 -07:00
teardown(() => {
2016-06-06 12:20:31 -07:00
contentStubs = null;
window.getSelection().removeAllRanges();
});
2017-05-16 14:17:14 -07:00
test('single line', () => {
const content = stubContent(138, 'left');
2016-06-06 12:20:31 -07:00
emulateSelection(content.firstChild, 5, content.firstChild, 12);
assert.isTrue(element.isRangeSelected());
assert.deepEqual(getActionRange(), {
startLine: 138,
startChar: 5,
endLine: 138,
endChar: 12,
});
assert.equal(getActionSide(), 'left');
});
2017-05-16 14:17:14 -07:00
test('multiline', () => {
const startContent = stubContent(119, 'right');
const endContent = stubContent(120, 'right');
2016-06-28 14:48:33 -07:00
emulateSelection(
startContent.firstChild, 10, endContent.lastChild, 7);
assert.isTrue(element.isRangeSelected());
assert.deepEqual(getActionRange(), {
startLine: 119,
startChar: 10,
endLine: 120,
2017-02-06 10:47:53 -08:00
endChar: 36,
2016-06-28 14:48:33 -07:00
});
assert.equal(getActionSide(), 'right');
});
2017-05-16 14:17:14 -07:00
test('multiline grow end highlight over tabs', () => {
const startContent = stubContent(119, 'right');
const endContent = stubContent(120, 'right');
2016-06-06 12:20:31 -07:00
emulateSelection(startContent.firstChild, 10, endContent.firstChild, 2);
assert.isTrue(element.isRangeSelected());
assert.deepEqual(getActionRange(), {
startLine: 119,
startChar: 10,
endLine: 120,
endChar: 2,
});
assert.equal(getActionSide(), 'right');
});
2017-05-16 14:17:14 -07:00
test('collapsed', () => {
const content = stubContent(138, 'left');
2016-06-06 12:20:31 -07:00
emulateSelection(content.firstChild, 5, content.firstChild, 5);
assert.isOk(window.getSelection().getRangeAt(0).startContainer);
assert.isFalse(element.isRangeSelected());
});
2017-05-16 14:17:14 -07:00
test('starts inside hl', () => {
const content = stubContent(140, 'left');
const hl = content.querySelector('.foo');
2016-06-06 12:20:31 -07:00
emulateSelection(hl.firstChild, 2, hl.nextSibling, 7);
assert.isTrue(element.isRangeSelected());
assert.deepEqual(getActionRange(), {
startLine: 140,
startChar: 8,
endLine: 140,
endChar: 23,
});
assert.equal(getActionSide(), 'left');
});
2017-05-16 14:17:14 -07:00
test('ends inside hl', () => {
const content = stubContent(140, 'left');
const hl = content.querySelector('.bar');
2016-06-06 12:20:31 -07:00
emulateSelection(hl.previousSibling, 2, hl.firstChild, 3);
assert.isTrue(element.isRangeSelected());
assert.deepEqual(getActionRange(), {
startLine: 140,
startChar: 18,
endLine: 140,
endChar: 27,
});
});
2017-05-16 14:17:14 -07:00
test('multiple hl', () => {
const content = stubContent(140, 'left');
const hl = content.querySelectorAll('hl')[4];
2016-06-06 12:20:31 -07:00
emulateSelection(content.firstChild, 2, hl.firstChild, 2);
assert.isTrue(element.isRangeSelected());
assert.deepEqual(getActionRange(), {
startLine: 140,
startChar: 2,
endLine: 140,
2017-02-06 10:47:53 -08:00
endChar: 61,
2016-06-06 12:20:31 -07:00
});
assert.equal(getActionSide(), 'left');
});
2017-05-16 14:17:14 -07:00
test('starts outside of diff', () => {
const contentText = stubContent(140, 'left');
const contentTd = contentText.parentElement;
2016-07-01 09:30:44 -07:00
2017-02-06 10:47:53 -08:00
emulateSelection(contentTd.previousElementSibling, 0,
2016-07-01 09:30:44 -07:00
contentText.firstChild, 2);
2016-06-06 12:20:31 -07:00
assert.isFalse(element.isRangeSelected());
});
2017-05-16 14:17:14 -07:00
test('ends outside of diff', () => {
const content = stubContent(140, 'left');
2016-06-06 12:20:31 -07:00
emulateSelection(content.nextElementSibling.firstChild, 2,
content.firstChild, 2);
assert.isFalse(element.isRangeSelected());
});
2017-05-16 14:17:14 -07:00
test('starts and ends on different sides', () => {
const startContent = stubContent(140, 'left');
const endContent = stubContent(130, 'right');
2016-06-06 12:20:31 -07:00
emulateSelection(startContent.firstChild, 2, endContent.firstChild, 2);
assert.isFalse(element.isRangeSelected());
});
2017-05-16 14:17:14 -07:00
test('starts in comment thread element', () => {
const startContent = stubContent(140, 'left');
const comment = startContent.parentElement.querySelector(
2016-07-01 09:30:44 -07:00
'gr-diff-comment-thread');
2017-05-16 14:17:14 -07:00
const endContent = stubContent(141, 'left');
2016-06-06 12:20:31 -07:00
emulateSelection(comment.firstChild, 2, endContent.firstChild, 4);
assert.isTrue(element.isRangeSelected());
assert.deepEqual(getActionRange(), {
startLine: 140,
2017-02-06 10:47:53 -08:00
startChar: 83,
2016-06-06 12:20:31 -07:00
endLine: 141,
endChar: 4,
});
assert.equal(getActionSide(), 'left');
});
2017-05-16 14:17:14 -07:00
test('ends in comment thread element', () => {
const content = stubContent(140, 'left');
const comment = content.parentElement.querySelector(
2016-07-01 09:30:44 -07:00
'gr-diff-comment-thread');
2016-06-06 12:20:31 -07:00
emulateSelection(content.firstChild, 4, comment.firstChild, 1);
assert.isTrue(element.isRangeSelected());
assert.deepEqual(getActionRange(), {
startLine: 140,
startChar: 4,
endLine: 140,
2017-02-06 10:47:53 -08:00
endChar: 83,
2016-06-06 12:20:31 -07:00
});
assert.equal(getActionSide(), 'left');
});
2017-05-16 14:17:14 -07:00
test('starts in context element', () => {
const contextControl =
2017-02-06 10:47:53 -08:00
diff.querySelector('.contextControl').querySelector('gr-button');
2017-05-16 14:17:14 -07:00
const content = stubContent(146, 'right');
2016-06-06 12:20:31 -07:00
emulateSelection(contextControl, 0, content.firstChild, 7);
// TODO (viktard): Select nearest line.
assert.isFalse(element.isRangeSelected());
});
2017-05-16 14:17:14 -07:00
test('ends in context element', () => {
const contextControl =
2017-02-06 10:47:53 -08:00
diff.querySelector('.contextControl').querySelector('gr-button');
2017-05-16 14:17:14 -07:00
const content = stubContent(141, 'left');
2017-02-06 10:47:53 -08:00
emulateSelection(content.firstChild, 2, contextControl, 1);
2016-06-06 12:20:31 -07:00
// TODO (viktard): Select nearest line.
assert.isFalse(element.isRangeSelected());
});
2017-05-16 14:17:14 -07:00
test('selection containing context element', () => {
const startContent = stubContent(130, 'right');
const endContent = stubContent(146, 'right');
2016-06-06 12:20:31 -07:00
emulateSelection(startContent.firstChild, 3, endContent.firstChild, 14);
assert.isTrue(element.isRangeSelected());
assert.deepEqual(getActionRange(), {
startLine: 130,
startChar: 3,
endLine: 146,
endChar: 14,
});
assert.equal(getActionSide(), 'right');
});
2017-05-16 14:17:14 -07:00
test('ends at a tab', () => {
const content = stubContent(140, 'left');
2016-06-28 14:48:33 -07:00
emulateSelection(
content.firstChild, 1, content.querySelector('span'), 0);
assert.isTrue(element.isRangeSelected());
assert.deepEqual(getActionRange(), {
startLine: 140,
startChar: 1,
endLine: 140,
endChar: 51,
});
assert.equal(getActionSide(), 'left');
});
2017-05-16 14:17:14 -07:00
test('starts at a tab', () => {
const content = stubContent(140, 'left');
2016-06-28 14:48:33 -07:00
emulateSelection(
content.querySelectorAll('hl')[3], 0,
2017-02-06 10:47:53 -08:00
content.querySelectorAll('span')[1].nextSibling, 1);
2016-06-28 14:48:33 -07:00
assert.isTrue(element.isRangeSelected());
assert.deepEqual(getActionRange(), {
startLine: 140,
startChar: 51,
endLine: 140,
2017-02-06 10:47:53 -08:00
endChar: 71,
2016-06-28 14:48:33 -07:00
});
assert.equal(getActionSide(), 'left');
});
2017-05-16 14:17:14 -07:00
test('properly accounts for syntax highlighting', () => {
const content = stubContent(140, 'left');
const spy = sinon.spy(element, '_normalizeRange');
2016-09-01 12:50:30 -07:00
emulateSelection(
content.querySelectorAll('hl')[3], 0,
content.querySelectorAll('span')[1], 0);
2017-05-16 14:17:14 -07:00
const spyCall = spy.getCall(0);
const range = window.getSelection().getRangeAt(0);
2016-09-01 12:50:30 -07:00
assert.notDeepEqual(spyCall.returnValue, range);
});
2017-05-16 14:17:14 -07:00
test('GrRangeNormalizer._getTextOffset computes text offset', () => {
let content = stubContent(140, 'left');
let child = content.lastChild.lastChild;
let result = GrRangeNormalizer._getTextOffset(content, child);
2017-02-06 10:47:53 -08:00
assert.equal(result, 75);
2016-09-01 12:50:30 -07:00
content = stubContent(146, 'right');
child = content.lastChild;
2016-09-13 15:23:25 -07:00
result = GrRangeNormalizer._getTextOffset(content, child);
2016-09-01 12:50:30 -07:00
assert.equal(result, 0);
});
2016-06-06 12:20:31 -07:00
// TODO (viktard): Selection starts in line number.
// TODO (viktard): Empty lines in selection start.
// TODO (viktard): Empty lines in selection end.
// TODO (viktard): Only empty lines selected.
// TODO (viktard): Unified mode.
2017-02-06 10:47:53 -08:00
2017-05-16 14:17:14 -07:00
suite('triple click', () => {
test('_fixTripleClickSelection', () => {
const fakeRange = {
2017-02-06 10:47:53 -08:00
startContainer: '',
startOffset: '',
endContainer: '',
2017-05-16 14:17:14 -07:00
endOffset: '',
2017-02-06 10:47:53 -08:00
};
2017-05-16 14:17:14 -07:00
const fixedRange = {};
2017-02-06 10:47:53 -08:00
sandbox.stub(GrRangeNormalizer, 'normalize').returns(fakeRange);
sandbox.stub(element, '_normalizeSelectionSide');
sandbox.stub(element, '_fixTripleClickSelection').returns(fixedRange);
assert.strictEqual(element._normalizeRange({}), fixedRange);
assert.isTrue(element._fixTripleClickSelection.called);
});
2017-05-16 14:17:14 -07:00
test('left pane', () => {
const startNode = stubContent(138, 'left');
const endNode =
2017-02-06 10:47:53 -08:00
stubContent(119, 'right').parentElement.previousElementSibling;
builder.getLineNumberByChild.withArgs(endNode).returns(119);
emulateSelection(startNode, 0, endNode, 0);
assert.deepEqual(getActionRange(), {
startLine: 138,
startChar: 0,
endLine: 138,
endChar: 63,
});
});
2017-05-16 14:17:14 -07:00
test('right pane', () => {
const startNode = stubContent(119, 'right');
const endNode =
2017-02-06 10:47:53 -08:00
stubContent(140, 'left').parentElement.previousElementSibling;
emulateSelection(startNode, 0, endNode, 0);
assert.deepEqual(getActionRange(), {
startLine: 119,
startChar: 0,
endLine: 119,
endChar: 63,
});
});
});
2016-06-06 12:20:31 -07:00
});
2016-06-10 12:47:57 -07:00
});
< / script >