| <!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">in physicis, quibus maxime gloriatur, primum totus est alienus. Democritea dicit</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-mouse-over from line comments is ignored', () => { |
| sandbox.stub(element, 'set'); |
| element.fire('comment-mouse-over', {comment: {}}); |
| assert.isFalse(element.set.called); |
| }); |
| |
| test('comment-mouse-over from ranged comment causes set', () => { |
| sandbox.stub(element, 'set'); |
| sandbox.stub(element, '_indexOfComment').returns(0); |
| element.fire('comment-mouse-over', {comment: {range: {}}}); |
| assert.isTrue(element.set.called); |
| }); |
| |
| test('comment-mouse-out from line comments is ignored', () => { |
| element.fire('comment-mouse-over', {comment: {}}); |
| assert.isFalse(builder.getContentsByLineRange.called); |
| }); |
| |
| test('on create-comment action box is removed', () => { |
| sandbox.stub(element, '_removeActionBox'); |
| element.fire('create-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(), { |
| startLine: 138, |
| startChar: 5, |
| endLine: 138, |
| endChar: 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(), { |
| startLine: 119, |
| startChar: 10, |
| endLine: 120, |
| endChar: 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(), { |
| startLine: 119, |
| startChar: 10, |
| endLine: 120, |
| endChar: 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(), { |
| startLine: 119, |
| startChar: 10, |
| endLine: 120, |
| endChar: 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(), { |
| startLine: 140, |
| startChar: 8, |
| endLine: 140, |
| endChar: 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(), { |
| startLine: 140, |
| startChar: 18, |
| endLine: 140, |
| endChar: 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(), { |
| startLine: 140, |
| startChar: 2, |
| endLine: 140, |
| endChar: 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(), { |
| startLine: 140, |
| startChar: 83, |
| endLine: 141, |
| endChar: 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(), { |
| startLine: 140, |
| startChar: 4, |
| endLine: 140, |
| endChar: 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(), { |
| startLine: 130, |
| startChar: 3, |
| endLine: 146, |
| endChar: 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(), { |
| startLine: 140, |
| startChar: 1, |
| endLine: 140, |
| endChar: 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(), { |
| startLine: 140, |
| startChar: 51, |
| endLine: 140, |
| endChar: 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); |
| }); |
| |
| // 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. |
| |
| suite('triple click', () => { |
| test('_fixTripleClickSelection', () => { |
| const fakeRange = { |
| startContainer: '', |
| startOffset: '', |
| endContainer: '', |
| endOffset: '', |
| }; |
| const fixedRange = {}; |
| 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); |
| }); |
| |
| test('left pane', () => { |
| const startNode = stubContent(138, 'left'); |
| const endNode = |
| 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, |
| }); |
| }); |
| |
| test('right pane', () => { |
| const startNode = stubContent(119, 'right'); |
| const endNode = |
| stubContent(140, 'left').parentElement.previousElementSibling; |
| emulateSelection(startNode, 0, endNode, 0); |
| assert.deepEqual(getActionRange(), { |
| startLine: 119, |
| startChar: 0, |
| endLine: 119, |
| endChar: 63, |
| }); |
| }); |
| }); |
| }); |
| }); |
| </script> |