| /** | |
| * @license | |
| * Copyright 2016 Google LLC | |
| * SPDX-License-Identifier: Apache-2.0 | |
| */ | |
| import '../../../test/common-test-setup'; | |
| import './gr-diff-highlight'; | |
| import {getTextOffset} from './gr-range-normalizer'; | |
| import {fixture, fixtureCleanup, html, assert} from '@open-wc/testing'; | |
| import {GrDiffHighlight, DiffBuilderInterface} from './gr-diff-highlight'; | |
| import {Side} from '../../../api/diff'; | |
| import {SinonStubbedMember} from 'sinon'; | |
| import {queryAndAssert} from '../../../utils/common-util'; | |
| import { | |
| GrDiffThreadElement, | |
| getDataFromCommentThreadEl, | |
| } from '../gr-diff/gr-diff-utils'; | |
| import { | |
| stubElement, | |
| waitQueryAndAssert, | |
| waitUntil, | |
| } from '../../../test/test-utils'; | |
| import {GrSelectionActionBox} from '../gr-selection-action-box/gr-selection-action-box'; | |
| import {DiffModel} from '../gr-diff-model/gr-diff-model'; | |
| // Splitting long lines in html into shorter rows breaks tests: | |
| // zero-length text nodes and new lines are not expected in some places | |
| /* eslint-disable max-len, lit/prefer-static-styles */ | |
| /* prettier-ignore */ | |
| const diffTable = html` | |
| <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 range generated_id314">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"><!-- a comment node -->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;">\u0009</span></hl>udiam, <hl>quid</hl> sit, <span class="tab-indicator" style="tab-size:8;">\u0009</span>quod <hl>Epicurum</hl></div><div class="comment-thread"> | |
| [Yet another random diff thread content here] | |
| </div></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;">\u0009</span></hl> otiosum, <span class="tab-indicator" style="tab-size:8;">\u0009</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, quodintellegam;</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"></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"></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> | |
| `; | |
| /* eslint-enable max-len */ | |
| suite('gr-diff-highlight', () => { | |
| suite('comment events', () => { | |
| let threadEl: GrDiffThreadElement; | |
| let hlRange: HTMLElement; | |
| let element: GrDiffHighlight; | |
| let diff: HTMLElement; | |
| let builder: DiffBuilderInterface; | |
| setup(async () => { | |
| diff = await fixture<HTMLTableElement>(diffTable); | |
| builder = { | |
| getContentTdByLineEl: sinon.stub(), | |
| diffModel: new DiffModel(document), | |
| }; | |
| element = new GrDiffHighlight(); | |
| element.init(diff, builder); | |
| hlRange = queryAndAssert(diff, 'hl.range.generated_id314'); | |
| threadEl = document.createElement( | |
| 'div' | |
| ) as unknown as GrDiffThreadElement; | |
| threadEl.className = 'comment-thread'; | |
| threadEl.rootId = 'id314'; | |
| threadEl.setAttribute('line-num', '12'); | |
| threadEl.setAttribute('diff-side', 'right'); | |
| diff.appendChild(threadEl); | |
| }); | |
| teardown(() => { | |
| element.cleanup(); | |
| threadEl.remove(); | |
| }); | |
| test('comment-thread-mouseenter toggles rangeHoverHighlight class', async () => { | |
| assert.isFalse(hlRange.classList.contains('rangeHoverHighlight')); | |
| threadEl.dispatchEvent( | |
| new CustomEvent('comment-thread-mouseenter', { | |
| detail: getDataFromCommentThreadEl(threadEl), | |
| bubbles: true, | |
| composed: true, | |
| }) | |
| ); | |
| await waitUntil(() => hlRange.classList.contains('rangeHoverHighlight')); | |
| assert.isTrue(hlRange.classList.contains('rangeHoverHighlight')); | |
| }); | |
| test('comment-thread-mouseleave toggles rangeHoverHighlight class', async () => { | |
| hlRange.classList.add('rangeHoverHighlight'); | |
| threadEl.dispatchEvent( | |
| new CustomEvent('comment-thread-mouseleave', { | |
| detail: getDataFromCommentThreadEl(threadEl), | |
| bubbles: true, | |
| composed: true, | |
| }) | |
| ); | |
| await waitUntil(() => !hlRange.classList.contains('rangeHoverHighlight')); | |
| assert.isFalse(hlRange.classList.contains('rangeHoverHighlight')); | |
| }); | |
| test(`create-range-comment for range when create-comment-requested | |
| is fired`, () => { | |
| const removeActionBoxStub = sinon.stub(element, 'removeActionBox'); | |
| const range = { | |
| start_line: 7, | |
| start_character: 11, | |
| end_line: 24, | |
| end_character: 42, | |
| }; | |
| element.selectedRange = {side: Side.LEFT, range}; | |
| const createCommentStub = sinon.stub( | |
| builder.diffModel, | |
| 'createCommentOnRange' | |
| ); | |
| diff.dispatchEvent(new CustomEvent('create-comment-requested')); | |
| assert.isTrue(createCommentStub.called); | |
| assert.deepEqual(createCommentStub.lastCall.args[0], range); | |
| assert.equal(createCommentStub.lastCall.args[1], Side.LEFT); | |
| assert.isTrue(removeActionBoxStub.called); | |
| }); | |
| }); | |
| suite('selection', () => { | |
| let element: GrDiffHighlight; | |
| let diff: HTMLElement; | |
| let getContentTdByLineElStub: SinonStubbedMember< | |
| DiffBuilderInterface['getContentTdByLineEl'] | |
| >; | |
| let contentStubs; | |
| setup(async () => { | |
| diff = await fixture<HTMLTableElement>(diffTable); | |
| const builder: DiffBuilderInterface = { | |
| getContentTdByLineEl: () => undefined, | |
| diffModel: new DiffModel(document), | |
| }; | |
| getContentTdByLineElStub = sinon.stub(builder, 'getContentTdByLineEl'); | |
| element = new GrDiffHighlight(); | |
| element.init(diff, builder); | |
| contentStubs = []; | |
| stubElement('gr-selection-action-box', 'placeAbove'); | |
| stubElement('gr-selection-action-box', 'placeBelow'); | |
| }); | |
| teardown(() => { | |
| fixtureCleanup(); | |
| element.cleanup(); | |
| contentStubs = null; | |
| document.getSelection()!.removeAllRanges(); | |
| }); | |
| const stubContent = (line: number, side: Side) => { | |
| const contentTd = diff.querySelector( | |
| `.${side}.lineNum[data-value="${line}"] ~ .content` | |
| ); | |
| if (!contentTd) assert.fail('content td not found'); | |
| const contentText = contentTd.querySelector('.contentText'); | |
| const lineEl = | |
| diff.querySelector(`.${side}.lineNum[data-value="${line}"]`) ?? | |
| undefined; | |
| contentStubs.push({ | |
| lineEl, | |
| contentTd, | |
| contentText, | |
| }); | |
| getContentTdByLineElStub.withArgs(lineEl).returns(contentTd); | |
| return contentText; | |
| }; | |
| const emulateSelection = ( | |
| startNode: Node, | |
| startOffset: number, | |
| endNode: Node, | |
| endOffset: number | |
| ) => { | |
| const selection = document.getSelection(); | |
| if (!selection) assert.fail('no selection'); | |
| selection.removeAllRanges(); | |
| const range = document.createRange(); | |
| range.setStart(startNode, startOffset); | |
| range.setEnd(endNode, endOffset); | |
| selection.addRange(range); | |
| element.handleSelection(selection, false); | |
| }; | |
| test('single first line', () => { | |
| const content = stubContent(1, Side.RIGHT); | |
| sinon.spy(element, 'positionActionBox'); | |
| if (!content?.firstChild) assert.fail('content first child not found'); | |
| emulateSelection(content.firstChild, 5, content.firstChild, 12); | |
| const actionBox = diff.querySelector('gr-selection-action-box'); | |
| if (!actionBox) assert.fail('action box not found'); | |
| assert.isTrue(actionBox.positionBelow); | |
| }); | |
| test('multiline starting on first line', () => { | |
| const startContent = stubContent(1, Side.RIGHT); | |
| const endContent = stubContent(2, Side.RIGHT); | |
| sinon.spy(element, 'positionActionBox'); | |
| if (!startContent?.firstChild) { | |
| assert.fail('first child of start content not found'); | |
| } | |
| if (!endContent?.lastChild) { | |
| assert.fail('last child of end content not found'); | |
| } | |
| emulateSelection(startContent.firstChild, 10, endContent.lastChild, 7); | |
| const actionBox = diff.querySelector('gr-selection-action-box'); | |
| if (!actionBox) assert.fail('action box not found'); | |
| assert.isTrue(actionBox.positionBelow); | |
| }); | |
| test('single line', async () => { | |
| const content = stubContent(138, Side.LEFT); | |
| sinon.spy(element, 'positionActionBox'); | |
| if (!content?.firstChild) assert.fail('content first child not found'); | |
| emulateSelection(content.firstChild, 5, content.firstChild, 12); | |
| const actionBox = await waitQueryAndAssert<GrSelectionActionBox>( | |
| diff, | |
| 'gr-selection-action-box' | |
| ); | |
| if (!element.selectedRange) assert.fail('no range selected'); | |
| const {range, side} = element.selectedRange; | |
| assert.deepEqual(range, { | |
| start_line: 138, | |
| start_character: 5, | |
| end_line: 138, | |
| end_character: 12, | |
| }); | |
| assert.equal(side, Side.LEFT); | |
| assert.notOk(actionBox.positionBelow); | |
| }); | |
| test('multiline', () => { | |
| const startContent = stubContent(119, Side.RIGHT); | |
| const endContent = stubContent(120, Side.RIGHT); | |
| sinon.spy(element, 'positionActionBox'); | |
| if (!startContent?.firstChild) { | |
| assert.fail('first child of start content not found'); | |
| } | |
| if (!endContent?.lastChild) { | |
| assert.fail('last child of end content'); | |
| } | |
| emulateSelection(startContent.firstChild, 10, endContent.lastChild, 7); | |
| const actionBox = diff.querySelector('gr-selection-action-box'); | |
| if (!actionBox) assert.fail('action box not found'); | |
| if (!element.selectedRange) assert.fail('no range selected'); | |
| const {range, side} = element.selectedRange; | |
| assert.deepEqual(range, { | |
| start_line: 119, | |
| start_character: 10, | |
| end_line: 120, | |
| end_character: 36, | |
| }); | |
| assert.equal(side, Side.RIGHT); | |
| assert.notOk(actionBox.positionBelow); | |
| }); | |
| test('multiple ranges aka firefox implementation', () => { | |
| const startContent = stubContent(119, Side.RIGHT); | |
| const endContent = stubContent(120, Side.RIGHT); | |
| if (!startContent?.firstChild) { | |
| assert.fail('first child of start content not found'); | |
| } | |
| if (!endContent?.lastChild) { | |
| assert.fail('last child of end content'); | |
| } | |
| 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 = sinon.stub(); | |
| getRangeAtStub | |
| .onFirstCall() | |
| .returns(startRange) | |
| .onSecondCall() | |
| .returns(endRange); | |
| const selection = { | |
| rangeCount: 2, | |
| getRangeAt: getRangeAtStub, | |
| removeAllRanges: sinon.stub(), | |
| } as unknown as Selection; | |
| element.handleSelection(selection, false); | |
| if (!element.selectedRange) assert.fail('no range selected'); | |
| const {range} = element.selectedRange; | |
| assert.deepEqual(range, { | |
| start_line: 119, | |
| start_character: 10, | |
| end_line: 120, | |
| end_character: 36, | |
| }); | |
| }); | |
| test('multiline grow end highlight over tabs', () => { | |
| const startContent = stubContent(119, Side.RIGHT); | |
| const endContent = stubContent(120, Side.RIGHT); | |
| if (!startContent?.firstChild) { | |
| assert.fail('first child of start content not found'); | |
| } | |
| if (!endContent?.firstChild) { | |
| assert.fail('first child of end content not found'); | |
| } | |
| emulateSelection(startContent.firstChild, 10, endContent.firstChild, 2); | |
| if (!element.selectedRange) assert.fail('no range selected'); | |
| const {range, side} = element.selectedRange; | |
| assert.deepEqual(range, { | |
| start_line: 119, | |
| start_character: 10, | |
| end_line: 120, | |
| end_character: 2, | |
| }); | |
| assert.equal(side, Side.RIGHT); | |
| }); | |
| test('collapsed', () => { | |
| const content = stubContent(138, Side.LEFT); | |
| if (!content?.firstChild) { | |
| assert.fail('first child of content not found'); | |
| } | |
| emulateSelection(content.firstChild, 5, content.firstChild, 5); | |
| const sel = document.getSelection(); | |
| if (!sel) assert.fail('no selection'); | |
| assert.isOk(sel.getRangeAt(0).startContainer); | |
| assert.isFalse(!!element.selectedRange); | |
| }); | |
| test('starts inside hl', () => { | |
| const content = stubContent(140, Side.LEFT); | |
| if (!content) { | |
| assert.fail('content not found'); | |
| } | |
| const hl = content.querySelector('.foo'); | |
| if (!hl?.firstChild) { | |
| assert.fail('first child of hl element not found'); | |
| } | |
| if (!hl?.nextSibling) { | |
| assert.fail('next sibling of hl element not found'); | |
| } | |
| emulateSelection(hl.firstChild, 2, hl.nextSibling, 7); | |
| if (!element.selectedRange) assert.fail('no range selected'); | |
| const {range, side} = element.selectedRange; | |
| assert.deepEqual(range, { | |
| start_line: 140, | |
| start_character: 8, | |
| end_line: 140, | |
| end_character: 23, | |
| }); | |
| assert.equal(side, Side.LEFT); | |
| }); | |
| test('ends inside hl', () => { | |
| const content = stubContent(140, Side.LEFT); | |
| if (!content) assert.fail('content not found'); | |
| const hl = content.querySelector('.bar'); | |
| if (!hl) assert.fail('hl inside content not found'); | |
| if (!hl.previousSibling) assert.fail('previous sibling not found'); | |
| if (!hl.firstChild) assert.fail('first child not found'); | |
| emulateSelection(hl.previousSibling, 2, hl.firstChild, 3); | |
| if (!element.selectedRange) assert.fail('no range selected'); | |
| const {range} = element.selectedRange; | |
| assert.deepEqual(range, { | |
| start_line: 140, | |
| start_character: 18, | |
| end_line: 140, | |
| end_character: 27, | |
| }); | |
| }); | |
| test('multiple hl', () => { | |
| const content = stubContent(140, Side.LEFT); | |
| if (!content) assert.fail('content not found'); | |
| if (!content.firstChild) assert.fail('first child not found'); | |
| const hl = content.querySelectorAll('hl')[4]; | |
| if (!hl) assert.fail('hl not found'); | |
| if (!hl.firstChild) assert.fail('first child of hl not found'); | |
| emulateSelection(content.firstChild, 2, hl.firstChild, 2); | |
| if (!element.selectedRange) assert.fail('no range selected'); | |
| const {range, side} = element.selectedRange; | |
| assert.deepEqual(range, { | |
| start_line: 140, | |
| start_character: 2, | |
| end_line: 140, | |
| end_character: 61, | |
| }); | |
| assert.equal(side, Side.LEFT); | |
| }); | |
| test('starts outside of diff', () => { | |
| const contentText = stubContent(140, Side.LEFT); | |
| if (!contentText) assert.fail('content not found'); | |
| if (!contentText.firstChild) assert.fail('child not found'); | |
| const contentTd = contentText.parentElement; | |
| if (!contentTd) assert.fail('content td not found'); | |
| if (!contentTd.parentElement) assert.fail('parent of td not found'); | |
| emulateSelection(contentTd.parentElement, 0, contentText.firstChild, 2); | |
| assert.isFalse(!!element.selectedRange); | |
| }); | |
| test('ends outside of diff', () => { | |
| const content = stubContent(140, Side.LEFT); | |
| if (!content) assert.fail('content not found'); | |
| if (!content.firstChild) assert.fail('child not found'); | |
| if (!content.nextElementSibling) assert.fail('sibling not found'); | |
| if (!content.nextElementSibling.firstChild) { | |
| assert.fail('sibling child not found'); | |
| } | |
| emulateSelection( | |
| content.nextElementSibling.firstChild, | |
| 2, | |
| content.firstChild, | |
| 2 | |
| ); | |
| assert.isFalse(!!element.selectedRange); | |
| }); | |
| test('starts and ends on different sides', () => { | |
| const startContent = stubContent(140, Side.LEFT); | |
| const endContent = stubContent(130, Side.RIGHT); | |
| if (!startContent?.firstChild) { | |
| assert.fail('first child of start content not found'); | |
| } | |
| if (!endContent?.firstChild) { | |
| assert.fail('first child of end content not found'); | |
| } | |
| emulateSelection(startContent.firstChild, 2, endContent.firstChild, 2); | |
| assert.isFalse(!!element.selectedRange); | |
| }); | |
| test('starts in comment thread element', () => { | |
| const startContent = stubContent(140, Side.LEFT); | |
| if (!startContent?.parentElement) { | |
| assert.fail('parent el of start content not found'); | |
| } | |
| const comment = | |
| startContent.parentElement.querySelector('.comment-thread'); | |
| if (!comment?.firstChild) { | |
| assert.fail('first child of comment not found'); | |
| } | |
| const endContent = stubContent(141, Side.LEFT); | |
| if (!endContent?.firstChild) { | |
| assert.fail('first child of end content not found'); | |
| } | |
| emulateSelection(comment.firstChild, 2, endContent.firstChild, 4); | |
| if (!element.selectedRange) assert.fail('no range selected'); | |
| const {range, side} = element.selectedRange; | |
| assert.deepEqual(range, { | |
| start_line: 140, | |
| start_character: 83, | |
| end_line: 141, | |
| end_character: 4, | |
| }); | |
| assert.equal(side, Side.LEFT); | |
| }); | |
| test('ends in comment thread element', () => { | |
| const content = stubContent(140, Side.LEFT); | |
| if (!content?.firstChild) { | |
| assert.fail('first child of content not found'); | |
| } | |
| if (!content?.parentElement) { | |
| assert.fail('parent element of content not found'); | |
| } | |
| const comment = content.parentElement.querySelector('.comment-thread'); | |
| if (!comment?.firstChild) { | |
| assert.fail('first child of comment element not found'); | |
| } | |
| emulateSelection(content.firstChild, 4, comment.firstChild, 1); | |
| if (!element.selectedRange) assert.fail('no range selected'); | |
| const {range, side} = element.selectedRange; | |
| assert.deepEqual(range, { | |
| start_line: 140, | |
| start_character: 4, | |
| end_line: 140, | |
| end_character: 83, | |
| }); | |
| assert.equal(side, Side.LEFT); | |
| }); | |
| test('starts in context element', () => { | |
| const contextControl = diff | |
| .querySelector('.contextControl')! | |
| .querySelector('gr-button'); | |
| if (!contextControl) assert.fail('context control not found'); | |
| const content = stubContent(146, Side.RIGHT); | |
| if (!content) assert.fail('content not found'); | |
| if (!content.firstChild) assert.fail('content child not found'); | |
| emulateSelection(contextControl, 0, content.firstChild, 7); | |
| // TODO (viktard): Select nearest line. | |
| assert.isFalse(!!element.selectedRange); | |
| }); | |
| test('ends in context element', () => { | |
| const contextControl = diff | |
| .querySelector('.contextControl')! | |
| .querySelector('gr-button'); | |
| if (!contextControl) { | |
| assert.fail('context control element not found'); | |
| } | |
| const content = stubContent(141, Side.LEFT); | |
| if (!content?.firstChild) { | |
| assert.fail('first child of content element not found'); | |
| } | |
| emulateSelection(content.firstChild, 2, contextControl, 1); | |
| // TODO (viktard): Select nearest line. | |
| assert.isFalse(!!element.selectedRange); | |
| }); | |
| test('selection containing context element', () => { | |
| const startContent = stubContent(130, Side.RIGHT); | |
| const endContent = stubContent(146, Side.RIGHT); | |
| if (!startContent?.firstChild) { | |
| assert.fail('first child of start content not found'); | |
| } | |
| if (!endContent?.firstChild) { | |
| assert.fail('first child of end content not found'); | |
| } | |
| emulateSelection(startContent.firstChild, 3, endContent.firstChild, 14); | |
| if (!element.selectedRange) assert.fail('no range selected'); | |
| const {range, side} = element.selectedRange; | |
| assert.deepEqual(range, { | |
| start_line: 130, | |
| start_character: 3, | |
| end_line: 146, | |
| end_character: 14, | |
| }); | |
| assert.equal(side, Side.RIGHT); | |
| }); | |
| test('ends at a tab', () => { | |
| const content = stubContent(140, Side.LEFT); | |
| if (!content?.firstChild) { | |
| assert.fail('first child of content element not found'); | |
| } | |
| const span = content.querySelector('span'); | |
| if (!span) assert.fail('span element not found'); | |
| emulateSelection(content.firstChild, 1, span, 0); | |
| if (!element.selectedRange) assert.fail('no range selected'); | |
| const {range, side} = element.selectedRange; | |
| assert.deepEqual(range, { | |
| start_line: 140, | |
| start_character: 1, | |
| end_line: 140, | |
| end_character: 51, | |
| }); | |
| assert.equal(side, Side.LEFT); | |
| }); | |
| test('starts at a tab', () => { | |
| const content = stubContent(140, Side.LEFT); | |
| if (!content) assert.fail('content element not found'); | |
| emulateSelection( | |
| content.querySelectorAll('hl')[3], | |
| 0, | |
| content.querySelectorAll('span')[1].nextSibling!, | |
| 1 | |
| ); | |
| if (!element.selectedRange) assert.fail('no range selected'); | |
| const {range, side} = element.selectedRange; | |
| assert.deepEqual(range, { | |
| start_line: 140, | |
| start_character: 51, | |
| end_line: 140, | |
| end_character: 71, | |
| }); | |
| assert.equal(side, Side.LEFT); | |
| }); | |
| test('properly accounts for syntax highlighting', () => { | |
| const content = stubContent(140, Side.LEFT); | |
| if (!content) assert.fail('content element not found'); | |
| emulateSelection( | |
| content.querySelectorAll('hl')[3], | |
| 0, | |
| content.querySelectorAll('span')[1], | |
| 0 | |
| ); | |
| if (!element.selectedRange) assert.fail('no range selected'); | |
| const {range, side} = element.selectedRange; | |
| assert.deepEqual(range, { | |
| start_line: 140, | |
| start_character: 51, | |
| end_line: 140, | |
| end_character: 69, | |
| }); | |
| assert.equal(side, Side.LEFT); | |
| }); | |
| test('GrRangeNormalizer.getTextOffset computes text offset', () => { | |
| let content = stubContent(140, Side.LEFT); | |
| if (!content) assert.fail('content element not found'); | |
| if (!content.lastChild) assert.fail('last child of content not found'); | |
| let child = content.lastChild.lastChild; | |
| if (!child) assert.fail('last child of last child of content not found'); | |
| let result = getTextOffset(content, child); | |
| assert.equal(result, 75); | |
| content = stubContent(146, Side.RIGHT); | |
| if (!content) assert.fail('content element not found'); | |
| child = content.lastChild; | |
| if (!child) assert.fail('child element not found'); | |
| result = getTextOffset(content, child); | |
| assert.equal(result, 0); | |
| }); | |
| test('fixTripleClickSelection', () => { | |
| const startContent = stubContent(119, Side.RIGHT); | |
| const endContent = stubContent(120, Side.RIGHT); | |
| if (!startContent?.firstChild) { | |
| assert.fail('first child of start content not found'); | |
| } | |
| if (!endContent) assert.fail('end content not found'); | |
| if (!endContent.firstChild) assert.fail('first child not found'); | |
| emulateSelection(startContent.firstChild, 0, endContent.firstChild, 0); | |
| if (!element.selectedRange) assert.fail('no range selected'); | |
| const {range, side} = element.selectedRange; | |
| assert.deepEqual(range, { | |
| start_line: 119, | |
| start_character: 0, | |
| end_line: 119, | |
| end_character: element.getLength(startContent), | |
| }); | |
| assert.equal(side, Side.RIGHT); | |
| }); | |
| }); | |
| }); |