| /** |
| * @license |
| * Copyright (C) 2021 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. |
| */ |
| |
| import '../../../test/common-test-setup-karma'; |
| import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions'; |
| import {Side, TokenHighlightEventDetails} from '../../../api/diff'; |
| import {GrDiffLine, GrDiffLineType} from '../gr-diff/gr-diff-line.js'; |
| import {HOVER_DELAY_MS, TokenHighlightLayer} from './token-highlight-layer'; |
| import {GrAnnotation} from '../gr-diff-highlight/gr-annotation'; |
| import {html, render} from 'lit'; |
| import {_testOnly_allTasks} from '../../../utils/async-util'; |
| import {queryAndAssert} from '../../../test/test-utils'; |
| |
| // MockInteractions.makeMouseEvent always sets buttons to 1. |
| function dispatchMouseEvent( |
| type: string, |
| xy: {x: number; y: number}, |
| node: Element |
| ) { |
| const props = { |
| bubbles: true, |
| cancellable: true, |
| composed: true, |
| clientX: xy.x, |
| clientY: xy.y, |
| buttons: 0, |
| }; |
| node.dispatchEvent(new MouseEvent(type, props)); |
| } |
| |
| class MockListener { |
| private results: any[][] = []; |
| |
| notify(...args: any[]) { |
| this.results.push(args); |
| } |
| |
| shift() { |
| return this.results.shift(); |
| } |
| |
| flush() { |
| this.results = []; |
| } |
| |
| get pending(): number { |
| return this.results.length; |
| } |
| } |
| |
| suite('token-highlight-layer', () => { |
| let container: HTMLElement; |
| let listener: MockListener; |
| let highlighter: TokenHighlightLayer; |
| let tokenHighlightingCalls: {details?: TokenHighlightEventDetails}[] = []; |
| |
| function tokenHighlightListener( |
| highlightDetails?: TokenHighlightEventDetails |
| ) { |
| tokenHighlightingCalls.push({details: highlightDetails}); |
| } |
| |
| setup(async () => { |
| listener = new MockListener(); |
| tokenHighlightingCalls = []; |
| container = document.createElement('div'); |
| document.body.appendChild(container); |
| highlighter = new TokenHighlightLayer(container, tokenHighlightListener); |
| highlighter.addListener((...args) => listener.notify(...args)); |
| }); |
| |
| teardown(() => { |
| document.body.removeChild(container); |
| }); |
| |
| function annotate(el: HTMLElement, side: Side = Side.LEFT, line = 1) { |
| const diffLine = new GrDiffLine(GrDiffLineType.BOTH); |
| diffLine.afterNumber = line; |
| diffLine.beforeNumber = line; |
| highlighter.annotate(el, document.createElement('div'), diffLine, side); |
| return el; |
| } |
| |
| let uniqueId = 0; |
| function createLineId() { |
| uniqueId++; |
| return `line-${uniqueId.toString()}`; |
| } |
| |
| function createLine(text: string, line = 1): HTMLElement { |
| const lineId = createLineId(); |
| const template = html` |
| <div class="line"> |
| <div data-value=${line} class="lineNum right"></div> |
| <div class="content"> |
| <div id=${lineId} class="contentText">${text}</div> |
| </div> |
| </div> |
| `; |
| |
| const div = document.createElement('div'); |
| render(template, div); |
| container.appendChild(div); |
| const el = queryAndAssert(container, `#${lineId}`); |
| return el as HTMLElement; |
| } |
| |
| suite('annotate', () => { |
| function assertAnnotation( |
| args: any[], |
| el: HTMLElement, |
| start: number, |
| length: number, |
| cssClass: string |
| ) { |
| assert.equal(args[0], el); |
| assert.equal(args[1], start); |
| assert.equal(args[2], length); |
| assert.equal(args[3], cssClass); |
| } |
| |
| test('annotate adds css token', () => { |
| const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement'); |
| const el = createLine('these are words'); |
| annotate(el); |
| assert.isTrue(annotateElementStub.calledThrice); |
| assertAnnotation(annotateElementStub.args[0], el, 0, 5, 'tk-these token'); |
| assertAnnotation(annotateElementStub.args[1], el, 6, 3, 'tk-are token'); |
| assertAnnotation( |
| annotateElementStub.args[2], |
| el, |
| 10, |
| 5, |
| 'tk-words token' |
| ); |
| }); |
| |
| test('annotate adds mouse handlers', () => { |
| const el = createLine('these are words'); |
| const addEventListenerStub = sinon.stub(el, 'addEventListener'); |
| annotate(el); |
| assert.isTrue(addEventListenerStub.calledTwice); |
| assert.equal(addEventListenerStub.args[0][0], 'mouseover'); |
| assert.equal(addEventListenerStub.args[1][0], 'mouseout'); |
| }); |
| |
| test('annotate does not add mouse handlers without words', () => { |
| const el = createLine(' '); |
| const addEventListenerStub = sinon.stub(el, 'addEventListener'); |
| annotate(el); |
| assert.isFalse(addEventListenerStub.called); |
| }); |
| |
| test('annotate adds mouse handlers for longest word', () => { |
| const el = createLine('w'.repeat(100)); |
| const addEventListenerStub = sinon.stub(el, 'addEventListener'); |
| annotate(el); |
| assert.isTrue(addEventListenerStub.called); |
| }); |
| |
| test('annotate does not add mouse handlers for long words', () => { |
| const el = createLine('w'.repeat(101)); |
| const addEventListenerStub = sinon.stub(el, 'addEventListener'); |
| annotate(el); |
| assert.isFalse(addEventListenerStub.called); |
| }); |
| }); |
| |
| suite('highlight', () => { |
| test('highlighting hover delay', async () => { |
| const clock = sinon.useFakeTimers(); |
| const line1 = createLine('two words'); |
| annotate(line1); |
| const line2 = createLine('three words'); |
| annotate(line2, Side.RIGHT, 2); |
| const words1 = queryAndAssert(line1, '.tk-words'); |
| assert.isTrue(words1.classList.contains('token')); |
| dispatchMouseEvent( |
| 'mouseover', |
| MockInteractions.middleOfNode(words1), |
| words1 |
| ); |
| |
| assert.equal(listener.pending, 0); |
| assert.equal(_testOnly_allTasks.size, 1); |
| |
| // Too early for hover behavior to trigger. |
| clock.tick(100); |
| assert.equal(listener.pending, 0); |
| assert.equal(_testOnly_allTasks.size, 1); |
| |
| // After a total of HOVER_DELAY_MS ms the hover behavior should trigger. |
| clock.tick(HOVER_DELAY_MS - 100); |
| assert.equal(listener.pending, 2); |
| assert.equal(_testOnly_allTasks.size, 0); |
| assert.deepEqual(listener.shift(), [1, 1, Side.LEFT]); |
| assert.deepEqual(listener.shift(), [2, 2, Side.RIGHT]); |
| }); |
| |
| test('highlighting spans many lines', async () => { |
| const clock = sinon.useFakeTimers(); |
| const line1 = createLine('two words'); |
| annotate(line1); |
| const line2 = createLine('three words'); |
| annotate(line2, Side.RIGHT, 1000); |
| const words1 = queryAndAssert(line1, '.tk-words'); |
| assert.isTrue(words1.classList.contains('token')); |
| dispatchMouseEvent( |
| 'mouseover', |
| MockInteractions.middleOfNode(words1), |
| words1 |
| ); |
| |
| assert.equal(listener.pending, 0); |
| |
| // After a total of HOVER_DELAY_MS ms the hover behavior should trigger. |
| clock.tick(HOVER_DELAY_MS); |
| assert.equal(listener.pending, 2); |
| assert.equal(_testOnly_allTasks.size, 0); |
| assert.deepEqual(listener.shift(), [1, 1, Side.LEFT]); |
| assert.deepEqual(listener.shift(), [1000, 1000, Side.RIGHT]); |
| }); |
| |
| test('highlighting mouse out before delay', async () => { |
| const clock = sinon.useFakeTimers(); |
| const line1 = createLine('two words'); |
| annotate(line1); |
| const line2 = createLine('three words', 2); |
| annotate(line2, Side.RIGHT, 2); |
| const words1 = queryAndAssert(line1, '.tk-words'); |
| assert.isTrue(words1.classList.contains('token')); |
| dispatchMouseEvent( |
| 'mouseover', |
| MockInteractions.middleOfNode(words1), |
| words1 |
| ); |
| assert.equal(listener.pending, 0); |
| clock.tick(100); |
| // Mouse out after 100ms but before hover delay. |
| dispatchMouseEvent( |
| 'mouseout', |
| MockInteractions.middleOfNode(words1), |
| words1 |
| ); |
| assert.equal(listener.pending, 0); |
| clock.tick(HOVER_DELAY_MS - 100); |
| assert.equal(listener.pending, 0); |
| assert.equal(_testOnly_allTasks.size, 0); |
| }); |
| |
| test('triggers listener for applying and clearing highlighting', async () => { |
| const clock = sinon.useFakeTimers(); |
| const line1 = createLine('two words'); |
| annotate(line1); |
| const line2 = createLine('three words', 2); |
| annotate(line2, Side.RIGHT, 2); |
| const words1 = queryAndAssert(line1, '.tk-words'); |
| assert.isTrue(words1.classList.contains('token')); |
| dispatchMouseEvent( |
| 'mouseover', |
| MockInteractions.middleOfNode(words1), |
| words1 |
| ); |
| assert.equal(tokenHighlightingCalls.length, 0); |
| clock.tick(HOVER_DELAY_MS); |
| assert.equal(tokenHighlightingCalls.length, 1); |
| assert.deepEqual(tokenHighlightingCalls[0].details, { |
| token: 'words', |
| side: Side.RIGHT, |
| element: words1, |
| range: {start_line: 1, start_column: 5, end_line: 1, end_column: 9}, |
| }); |
| |
| MockInteractions.click(container); |
| assert.equal(tokenHighlightingCalls.length, 2); |
| assert.deepEqual(tokenHighlightingCalls[1].details, undefined); |
| }); |
| |
| test('triggers listener on token with single occurrence', async () => { |
| const clock = sinon.useFakeTimers(); |
| const line1 = createLine('a tokenWithSingleOccurence'); |
| const line2 = createLine('can be highlighted', 2); |
| annotate(line1); |
| annotate(line2, Side.RIGHT, 2); |
| const tokenNode = queryAndAssert(line1, '.tk-tokenWithSingleOccurence'); |
| assert.isTrue(tokenNode.classList.contains('token')); |
| dispatchMouseEvent( |
| 'mouseover', |
| MockInteractions.middleOfNode(tokenNode), |
| tokenNode |
| ); |
| assert.equal(tokenHighlightingCalls.length, 0); |
| clock.tick(HOVER_DELAY_MS); |
| assert.equal(tokenHighlightingCalls.length, 1); |
| assert.deepEqual(tokenHighlightingCalls[0].details, { |
| token: 'tokenWithSingleOccurence', |
| side: Side.RIGHT, |
| element: tokenNode, |
| range: {start_line: 1, start_column: 3, end_line: 1, end_column: 26}, |
| }); |
| |
| MockInteractions.click(container); |
| assert.equal(tokenHighlightingCalls.length, 2); |
| assert.deepEqual(tokenHighlightingCalls[1].details, undefined); |
| }); |
| |
| test('clicking clears highlight', async () => { |
| const clock = sinon.useFakeTimers(); |
| const line1 = createLine('two words'); |
| annotate(line1); |
| const line2 = createLine('three words', 2); |
| annotate(line2, Side.RIGHT, 2); |
| const words1 = queryAndAssert(line1, '.tk-words'); |
| assert.isTrue(words1.classList.contains('token')); |
| dispatchMouseEvent( |
| 'mouseover', |
| MockInteractions.middleOfNode(words1), |
| words1 |
| ); |
| assert.equal(listener.pending, 0); |
| clock.tick(HOVER_DELAY_MS); |
| assert.equal(listener.pending, 2); |
| listener.flush(); |
| assert.equal(listener.pending, 0); |
| MockInteractions.click(container); |
| assert.equal(listener.pending, 2); |
| assert.deepEqual(listener.shift(), [1, 1, Side.LEFT]); |
| assert.deepEqual(listener.shift(), [2, 2, Side.RIGHT]); |
| }); |
| |
| test('clicking on word does not clear highlight', async () => { |
| const clock = sinon.useFakeTimers(); |
| const line1 = createLine('two words'); |
| annotate(line1); |
| const line2 = createLine('three words', 2); |
| annotate(line2, Side.RIGHT, 2); |
| const words1 = queryAndAssert(line1, '.tk-words'); |
| assert.isTrue(words1.classList.contains('token')); |
| dispatchMouseEvent( |
| 'mouseover', |
| MockInteractions.middleOfNode(words1), |
| words1 |
| ); |
| assert.equal(listener.pending, 0); |
| clock.tick(HOVER_DELAY_MS); |
| assert.equal(listener.pending, 2); |
| listener.flush(); |
| assert.equal(listener.pending, 0); |
| MockInteractions.click(words1); |
| assert.equal(listener.pending, 0); |
| }); |
| }); |
| }); |