| /** |
| * @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. |
| */ |
| |
| import '../../../test/common-test-setup-karma.js'; |
| import '../gr-diff/gr-diff-group.js'; |
| import './gr-diff-builder.js'; |
| import {getMockDiffResponse} from '../../../test/mocks/diff-response.js'; |
| import './gr-diff-builder-element.js'; |
| import {stubBaseUrl} from '../../../test/test-utils.js'; |
| import {flush} from '@polymer/polymer/lib/legacy/polymer.dom.js'; |
| import {GrAnnotation} from '../gr-diff-highlight/gr-annotation.js'; |
| import {GrDiffLine, GrDiffLineType} from '../gr-diff/gr-diff-line.js'; |
| import {GrDiffGroup, GrDiffGroupType} from '../gr-diff/gr-diff-group.js'; |
| import {GrDiffBuilder} from './gr-diff-builder.js'; |
| import {GrDiffBuilderSideBySide} from './gr-diff-builder-side-by-side.js'; |
| import {html} from '@polymer/polymer/lib/utils/html-tag.js'; |
| import {DiffViewMode} from '../../../api/diff.js'; |
| import {stubRestApi} from '../../../test/test-utils.js'; |
| |
| const basicFixture = fixtureFromTemplate(html` |
| <gr-diff-builder> |
| <table id="diffTable"></table> |
| </gr-diff-builder> |
| `); |
| |
| const divWithTextFixture = fixtureFromTemplate(html` |
| <div>Lorem ipsum dolor sit amet, suspendisse inceptos vehicula</div> |
| `); |
| |
| const mockDiffFixture = fixtureFromTemplate(html` |
| <gr-diff-builder view-mode="SIDE_BY_SIDE"> |
| <table id="diffTable"></table> |
| </gr-diff-builder> |
| `); |
| |
| suite('gr-diff-builder tests', () => { |
| let prefs; |
| let element; |
| let builder; |
| |
| const LINE_FEED_HTML = '<span class="style-scope gr-diff br"></span>'; |
| |
| setup(() => { |
| element = basicFixture.instantiate(); |
| stubRestApi('getLoggedIn').returns(Promise.resolve(false)); |
| stubRestApi('getProjectConfig').returns(Promise.resolve({})); |
| stubBaseUrl('/r'); |
| prefs = { |
| line_length: 10, |
| show_tabs: true, |
| tab_size: 4, |
| }; |
| builder = new GrDiffBuilder({content: []}, prefs); |
| }); |
| |
| test('_createElement classStr applies all classes', () => { |
| const node = builder._createElement('div', 'test classes'); |
| assert.isTrue(node.classList.contains('gr-diff')); |
| assert.isTrue(node.classList.contains('test')); |
| assert.isTrue(node.classList.contains('classes')); |
| }); |
| |
| suite('context control', () => { |
| function createContextGroups(options) { |
| const offset = options.offset || 0; |
| const numLines = options.count || 10; |
| const lines = []; |
| for (let i = 0; i < numLines; i++) { |
| const line = new GrDiffLine(GrDiffLineType.BOTH); |
| line.beforeNumber = offset + i + 1; |
| line.afterNumber = offset + i + 1; |
| line.text = 'lorem upsum'; |
| lines.push(line); |
| } |
| |
| return [new GrDiffGroup(GrDiffGroupType.BOTH, lines)]; |
| } |
| |
| function createContextSectionForGroups(options) { |
| const section = document.createElement('div'); |
| builder._createContextControls( |
| section, createContextGroups(options), DiffViewMode.UNIFIED); |
| return section; |
| } |
| |
| setup(() => { |
| builder = new GrDiffBuilder({content: []}, prefs, null, []); |
| }); |
| |
| test('no +10 buttons for 10 or less lines', () => { |
| const section = createContextSectionForGroups({count: 10}); |
| const buttons = section.querySelectorAll('gr-button.showContext'); |
| |
| assert.equal(buttons.length, 1); |
| assert.equal(buttons[0].textContent, '+10 common lines'); |
| }); |
| |
| test('context control at the top', () => { |
| builder._numLinesLeft = 50; |
| const section = createContextSectionForGroups({offset: 0, count: 20}); |
| const buttons = section.querySelectorAll('gr-button.showContext'); |
| |
| assert.equal(buttons.length, 2); |
| assert.equal(buttons[0].textContent, '+20 common lines'); |
| assert.equal(buttons[1].textContent, '+10'); |
| |
| assert.include([...buttons[0].classList.values()], 'belowButton'); |
| assert.include([...buttons[1].classList.values()], 'belowButton'); |
| }); |
| |
| test('context control in the middle', () => { |
| builder._numLinesLeft = 50; |
| const section = createContextSectionForGroups({offset: 10, count: 20}); |
| const buttons = section.querySelectorAll('gr-button.showContext'); |
| |
| assert.equal(buttons.length, 3); |
| assert.equal(buttons[0].textContent, '+20 common lines'); |
| assert.equal(buttons[1].textContent, '+10'); |
| assert.equal(buttons[2].textContent, '+10'); |
| |
| assert.include([...buttons[0].classList.values()], 'centeredButton'); |
| assert.include([...buttons[1].classList.values()], 'aboveButton'); |
| assert.include([...buttons[2].classList.values()], 'belowButton'); |
| }); |
| |
| test('context control at the bottom', () => { |
| builder._numLinesLeft = 50; |
| const section = createContextSectionForGroups({offset: 30, count: 20}); |
| const buttons = section.querySelectorAll('gr-button.showContext'); |
| |
| assert.equal(buttons.length, 2); |
| assert.equal(buttons[0].textContent, '+20 common lines'); |
| assert.equal(buttons[1].textContent, '+10'); |
| |
| assert.include([...buttons[0].classList.values()], 'aboveButton'); |
| assert.include([...buttons[1].classList.values()], 'aboveButton'); |
| }); |
| }); |
| |
| test('newlines 1', () => { |
| let text = 'abcdef'; |
| |
| assert.equal(builder._formatText(text, 4, 10).innerHTML, text); |
| text = 'a'.repeat(20); |
| assert.equal(builder._formatText(text, 4, 10).innerHTML, |
| 'a'.repeat(10) + |
| LINE_FEED_HTML + |
| 'a'.repeat(10)); |
| }); |
| |
| test('newlines 2', () => { |
| const text = '<span class="thumbsup">👍</span>'; |
| assert.equal(builder._formatText(text, 4, 10).innerHTML, |
| '<span clas' + |
| LINE_FEED_HTML + |
| 's="thumbsu' + |
| LINE_FEED_HTML + |
| 'p">👍</span' + |
| LINE_FEED_HTML + |
| '>'); |
| }); |
| |
| test('newlines 3', () => { |
| const text = '01234\t56789'; |
| assert.equal(builder._formatText(text, 4, 10).innerHTML, |
| '01234' + builder._getTabWrapper(3).outerHTML + '56' + |
| LINE_FEED_HTML + |
| '789'); |
| }); |
| |
| test('newlines 4', () => { |
| const text = '👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍'; |
| assert.equal(builder._formatText(text, 4, 20).innerHTML, |
| '👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍' + |
| LINE_FEED_HTML + |
| '👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍' + |
| LINE_FEED_HTML + |
| '👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍'); |
| }); |
| |
| test('line_length ignored if line_wrapping is true', () => { |
| builder._prefs = {line_wrapping: true, tab_size: 4, line_length: 50}; |
| const text = 'a'.repeat(51); |
| |
| const line = {text, highlights: []}; |
| const result = builder._createTextEl(undefined, line).firstChild.innerHTML; |
| assert.equal(result, text); |
| }); |
| |
| test('line_length applied if line_wrapping is false', () => { |
| builder._prefs = {line_wrapping: false, tab_size: 4, line_length: 50}; |
| const text = 'a'.repeat(51); |
| |
| const line = {text, highlights: []}; |
| const expected = 'a'.repeat(50) + LINE_FEED_HTML + 'a'; |
| const result = builder._createTextEl(undefined, line).firstChild.innerHTML; |
| assert.equal(result, expected); |
| }); |
| |
| [DiffViewMode.UNIFIED, DiffViewMode.SIDE_BY_SIDE] |
| .forEach(mode => { |
| test(`line_length used for regular files under ${mode}`, () => { |
| element.path = '/a.txt'; |
| element.viewMode = mode; |
| builder = element._getDiffBuilder( |
| {}, {tab_size: 4, line_length: 50} |
| ); |
| assert.equal(builder._prefs.line_length, 50); |
| }); |
| |
| test(`line_length ignored for commit msg under ${mode}`, () => { |
| element.path = '/COMMIT_MSG'; |
| element.viewMode = mode; |
| builder = element._getDiffBuilder( |
| {}, {tab_size: 4, line_length: 50} |
| ); |
| assert.equal(builder._prefs.line_length, 72); |
| }); |
| }); |
| |
| test('_createTextEl linewrap with tabs', () => { |
| const text = '\t'.repeat(7) + '!'; |
| const line = {text, highlights: []}; |
| const el = builder._createTextEl(undefined, line); |
| assert.equal(el.innerText, text); |
| // With line length 10 and tab size 2, there should be a line break |
| // after every two tabs. |
| const newlineEl = el.querySelector('.contentText > .br'); |
| assert.isOk(newlineEl); |
| assert.equal( |
| el.querySelector('.contentText .tab:nth-child(2)').nextSibling, |
| newlineEl); |
| }); |
| |
| test('text length with tabs and unicode', () => { |
| function expectTextLength(text, tabSize, expected) { |
| // Formatting to |expected| columns should not introduce line breaks. |
| const result = builder._formatText(text, tabSize, expected); |
| assert.isNotOk(result.querySelector('.contentText > .br'), |
| ` Expected the result of: \n` + |
| ` _formatText(${text}', ${tabSize}, ${expected})\n` + |
| ` to not contain a br. But the actual result HTML was:\n` + |
| ` '${result.innerHTML}'\nwhereupon`); |
| |
| // Increasing the line limit should produce the same markup. |
| assert.equal(builder._formatText(text, tabSize, Infinity).innerHTML, |
| result.innerHTML); |
| assert.equal(builder._formatText(text, tabSize, expected + 1).innerHTML, |
| result.innerHTML); |
| |
| // Decreasing the line limit should introduce line breaks. |
| if (expected > 0) { |
| const tooSmall = builder._formatText(text, tabSize, expected - 1); |
| assert.isOk(tooSmall.querySelector('.contentText > .br'), |
| ` Expected the result of: \n` + |
| ` _formatText(${text}', ${tabSize}, ${expected - 1})\n` + |
| ` to contain a br. But the actual result HTML was:\n` + |
| ` '${tooSmall.innerHTML}'\nwhereupon`); |
| } |
| } |
| expectTextLength('12345', 4, 5); |
| expectTextLength('\t\t12', 4, 10); |
| expectTextLength('abc💢123', 4, 7); |
| expectTextLength('abc\t', 8, 8); |
| expectTextLength('abc\t\t', 10, 20); |
| expectTextLength('', 10, 0); |
| // 17 Thai combining chars. |
| expectTextLength('ก้้้้้้้้้้้้้้้้', 4, 17); |
| expectTextLength('abc\tde', 10, 12); |
| expectTextLength('abc\tde\t', 10, 20); |
| expectTextLength('\t\t\t\t\t', 20, 100); |
| }); |
| |
| test('tab wrapper insertion', () => { |
| const html = 'abc\tdef'; |
| const tabSize = builder._prefs.tab_size; |
| const wrapper = builder._getTabWrapper(tabSize - 3); |
| assert.ok(wrapper); |
| assert.equal(wrapper.innerText, '\t'); |
| assert.equal( |
| builder._formatText(html, tabSize, Infinity).innerHTML, |
| 'abc' + wrapper.outerHTML + 'def'); |
| }); |
| |
| test('tab wrapper style', () => { |
| const pattern = new RegExp('^<span class="style-scope gr-diff tab" ' + |
| 'style="((?:-moz-)?tab-size: (\\d+);.?)+">\\t<\\/span>$'); |
| |
| for (const size of [1, 3, 8, 55]) { |
| const html = builder._getTabWrapper(size).outerHTML; |
| expect(html).to.match(pattern); |
| assert.equal(html.match(pattern)[2], size); |
| } |
| }); |
| |
| test('_handlePreferenceError throws with invalid preference', () => { |
| const prefs = {tab_size: 0}; |
| assert.throws(() => element._getDiffBuilder(element.diff, prefs)); |
| }); |
| |
| test('_handlePreferenceError triggers alert and javascript error', () => { |
| const errorStub = sinon.stub(); |
| element.addEventListener('show-alert', errorStub); |
| assert.throws(() => element._handlePreferenceError('tab size')); |
| assert.equal(errorStub.lastCall.args[0].detail.message, |
| `The value of the 'tab size' user preference is invalid. ` + |
| `Fix in diff preferences`); |
| }); |
| |
| suite('_isTotal', () => { |
| test('is total for add', () => { |
| const group = new GrDiffGroup(GrDiffGroupType.DELTA); |
| for (let idx = 0; idx < 10; idx++) { |
| group.addLine(new GrDiffLine(GrDiffLineType.ADD)); |
| } |
| assert.isTrue(GrDiffBuilder.prototype._isTotal(group)); |
| }); |
| |
| test('is total for remove', () => { |
| const group = new GrDiffGroup(GrDiffGroupType.DELTA); |
| for (let idx = 0; idx < 10; idx++) { |
| group.addLine(new GrDiffLine(GrDiffLineType.REMOVE)); |
| } |
| assert.isTrue(GrDiffBuilder.prototype._isTotal(group)); |
| }); |
| |
| test('not total for empty', () => { |
| const group = new GrDiffGroup(GrDiffGroupType.BOTH); |
| assert.isFalse(GrDiffBuilder.prototype._isTotal(group)); |
| }); |
| |
| test('not total for non-delta', () => { |
| const group = new GrDiffGroup(GrDiffGroupType.DELTA); |
| for (let idx = 0; idx < 10; idx++) { |
| group.addLine(new GrDiffLine(GrDiffLineType.BOTH)); |
| } |
| assert.isFalse(GrDiffBuilder.prototype._isTotal(group)); |
| }); |
| }); |
| |
| suite('intraline differences', () => { |
| let el; |
| let str; |
| let annotateElementSpy; |
| let layer; |
| const lineNumberEl = document.createElement('td'); |
| |
| function slice(str, start, end) { |
| return Array.from(str).slice(start, end) |
| .join(''); |
| } |
| |
| setup(() => { |
| el = divWithTextFixture.instantiate(); |
| str = el.textContent; |
| annotateElementSpy = sinon.spy(GrAnnotation, 'annotateElement'); |
| layer = document.createElement('gr-diff-builder') |
| ._createIntralineLayer(); |
| }); |
| |
| test('annotate no highlights', () => { |
| const line = { |
| text: str, |
| highlights: [], |
| }; |
| |
| layer.annotate(el, lineNumberEl, line); |
| |
| // The content is unchanged. |
| assert.isFalse(annotateElementSpy.called); |
| assert.equal(el.childNodes.length, 1); |
| assert.instanceOf(el.childNodes[0], Text); |
| assert.equal(str, el.childNodes[0].textContent); |
| }); |
| |
| test('annotate with highlights', () => { |
| const line = { |
| text: str, |
| highlights: [ |
| {startIndex: 6, endIndex: 12}, |
| {startIndex: 18, endIndex: 22}, |
| ], |
| }; |
| const str0 = slice(str, 0, 6); |
| const str1 = slice(str, 6, 12); |
| const str2 = slice(str, 12, 18); |
| const str3 = slice(str, 18, 22); |
| const str4 = slice(str, 22); |
| |
| layer.annotate(el, lineNumberEl, line); |
| |
| assert.isTrue(annotateElementSpy.called); |
| assert.equal(el.childNodes.length, 5); |
| |
| assert.instanceOf(el.childNodes[0], Text); |
| assert.equal(el.childNodes[0].textContent, str0); |
| |
| assert.notInstanceOf(el.childNodes[1], Text); |
| assert.equal(el.childNodes[1].textContent, str1); |
| |
| assert.instanceOf(el.childNodes[2], Text); |
| assert.equal(el.childNodes[2].textContent, str2); |
| |
| assert.notInstanceOf(el.childNodes[3], Text); |
| assert.equal(el.childNodes[3].textContent, str3); |
| |
| assert.instanceOf(el.childNodes[4], Text); |
| assert.equal(el.childNodes[4].textContent, str4); |
| }); |
| |
| test('annotate without endIndex', () => { |
| const line = { |
| text: str, |
| highlights: [ |
| {startIndex: 28}, |
| ], |
| }; |
| |
| const str0 = slice(str, 0, 28); |
| const str1 = slice(str, 28); |
| |
| layer.annotate(el, lineNumberEl, line); |
| |
| assert.isTrue(annotateElementSpy.called); |
| assert.equal(el.childNodes.length, 2); |
| |
| assert.instanceOf(el.childNodes[0], Text); |
| assert.equal(el.childNodes[0].textContent, str0); |
| |
| assert.notInstanceOf(el.childNodes[1], Text); |
| assert.equal(el.childNodes[1].textContent, str1); |
| }); |
| |
| test('annotate ignores empty highlights', () => { |
| const line = { |
| text: str, |
| highlights: [ |
| {startIndex: 28, endIndex: 28}, |
| ], |
| }; |
| |
| layer.annotate(el, lineNumberEl, line); |
| |
| assert.isFalse(annotateElementSpy.called); |
| assert.equal(el.childNodes.length, 1); |
| }); |
| |
| test('annotate handles unicode', () => { |
| // Put some unicode into the string: |
| str = str.replace(/\s/g, '💢'); |
| el.textContent = str; |
| const line = { |
| text: str, |
| highlights: [ |
| {startIndex: 6, endIndex: 12}, |
| ], |
| }; |
| |
| const str0 = slice(str, 0, 6); |
| const str1 = slice(str, 6, 12); |
| const str2 = slice(str, 12); |
| |
| layer.annotate(el, lineNumberEl, line); |
| |
| assert.isTrue(annotateElementSpy.called); |
| assert.equal(el.childNodes.length, 3); |
| |
| assert.instanceOf(el.childNodes[0], Text); |
| assert.equal(el.childNodes[0].textContent, str0); |
| |
| assert.notInstanceOf(el.childNodes[1], Text); |
| assert.equal(el.childNodes[1].textContent, str1); |
| |
| assert.instanceOf(el.childNodes[2], Text); |
| assert.equal(el.childNodes[2].textContent, str2); |
| }); |
| |
| test('annotate handles unicode w/o endIndex', () => { |
| // Put some unicode into the string: |
| str = str.replace(/\s/g, '💢'); |
| el.textContent = str; |
| |
| const line = { |
| text: str, |
| highlights: [ |
| {startIndex: 6}, |
| ], |
| }; |
| |
| const str0 = slice(str, 0, 6); |
| const str1 = slice(str, 6); |
| |
| layer.annotate(el, lineNumberEl, line); |
| |
| assert.isTrue(annotateElementSpy.called); |
| assert.equal(el.childNodes.length, 2); |
| |
| assert.instanceOf(el.childNodes[0], Text); |
| assert.equal(el.childNodes[0].textContent, str0); |
| |
| assert.notInstanceOf(el.childNodes[1], Text); |
| assert.equal(el.childNodes[1].textContent, str1); |
| }); |
| }); |
| |
| suite('tab indicators', () => { |
| let element; |
| let layer; |
| const lineNumberEl = document.createElement('td'); |
| |
| setup(() => { |
| element = basicFixture.instantiate(); |
| element._showTabs = true; |
| layer = element._createTabIndicatorLayer(); |
| }); |
| |
| test('does nothing with empty line', () => { |
| const line = {text: ''}; |
| const el = document.createElement('div'); |
| const annotateElementStub = |
| sinon.stub(GrAnnotation, 'annotateElement'); |
| |
| layer.annotate(el, lineNumberEl, line); |
| |
| assert.isFalse(annotateElementStub.called); |
| }); |
| |
| test('does nothing with no tabs', () => { |
| const str = 'lorem ipsum no tabs'; |
| const line = {text: str}; |
| const el = document.createElement('div'); |
| el.textContent = str; |
| const annotateElementStub = |
| sinon.stub(GrAnnotation, 'annotateElement'); |
| |
| layer.annotate(el, lineNumberEl, line); |
| |
| assert.isFalse(annotateElementStub.called); |
| }); |
| |
| test('annotates tab at beginning', () => { |
| const str = '\tlorem upsum'; |
| const line = {text: str}; |
| const el = document.createElement('div'); |
| el.textContent = str; |
| const annotateElementStub = |
| sinon.stub(GrAnnotation, 'annotateElement'); |
| |
| layer.annotate(el, lineNumberEl, line); |
| |
| assert.equal(annotateElementStub.callCount, 1); |
| const args = annotateElementStub.getCalls()[0].args; |
| assert.equal(args[0], el); |
| assert.equal(args[1], 0, 'offset of tab indicator'); |
| assert.equal(args[2], 1, 'length of tab indicator'); |
| assert.include(args[3], 'tab-indicator'); |
| }); |
| |
| test('does not annotate when disabled', () => { |
| element._showTabs = false; |
| |
| const str = '\tlorem upsum'; |
| const line = {text: str}; |
| const el = document.createElement('div'); |
| el.textContent = str; |
| const annotateElementStub = |
| sinon.stub(GrAnnotation, 'annotateElement'); |
| |
| layer.annotate(el, lineNumberEl, line); |
| |
| assert.isFalse(annotateElementStub.called); |
| }); |
| |
| test('annotates multiple in beginning', () => { |
| const str = '\t\tlorem upsum'; |
| const line = {text: str}; |
| const el = document.createElement('div'); |
| el.textContent = str; |
| const annotateElementStub = |
| sinon.stub(GrAnnotation, 'annotateElement'); |
| |
| layer.annotate(el, lineNumberEl, line); |
| |
| assert.equal(annotateElementStub.callCount, 2); |
| |
| let args = annotateElementStub.getCalls()[0].args; |
| assert.equal(args[0], el); |
| assert.equal(args[1], 0, 'offset of tab indicator'); |
| assert.equal(args[2], 1, 'length of tab indicator'); |
| assert.include(args[3], 'tab-indicator'); |
| |
| args = annotateElementStub.getCalls()[1].args; |
| assert.equal(args[0], el); |
| assert.equal(args[1], 1, 'offset of tab indicator'); |
| assert.equal(args[2], 1, 'length of tab indicator'); |
| assert.include(args[3], 'tab-indicator'); |
| }); |
| |
| test('annotates intermediate tabs', () => { |
| const str = 'lorem\tupsum'; |
| const line = {text: str}; |
| const el = document.createElement('div'); |
| el.textContent = str; |
| const annotateElementStub = |
| sinon.stub(GrAnnotation, 'annotateElement'); |
| |
| layer.annotate(el, lineNumberEl, line); |
| |
| assert.equal(annotateElementStub.callCount, 1); |
| const args = annotateElementStub.getCalls()[0].args; |
| assert.equal(args[0], el); |
| assert.equal(args[1], 5, 'offset of tab indicator'); |
| assert.equal(args[2], 1, 'length of tab indicator'); |
| assert.include(args[3], 'tab-indicator'); |
| }); |
| }); |
| |
| suite('layers', () => { |
| let element; |
| let initialLayersCount; |
| let withLayerCount; |
| setup(() => { |
| const layers = []; |
| element = basicFixture.instantiate(); |
| element.layers = layers; |
| element._showTrailingWhitespace = true; |
| element._setupAnnotationLayers(); |
| initialLayersCount = element._layers.length; |
| }); |
| |
| test('no layers', () => { |
| element._setupAnnotationLayers(); |
| assert.equal(element._layers.length, initialLayersCount); |
| }); |
| |
| suite('with layers', () => { |
| const layers = [{}, {}]; |
| setup(() => { |
| element = basicFixture.instantiate(); |
| element.layers = layers; |
| element._showTrailingWhitespace = true; |
| element._setupAnnotationLayers(); |
| withLayerCount = element._layers.length; |
| }); |
| test('with layers', () => { |
| element._setupAnnotationLayers(); |
| assert.equal(element._layers.length, withLayerCount); |
| assert.equal(initialLayersCount + layers.length, |
| withLayerCount); |
| }); |
| }); |
| }); |
| |
| suite('trailing whitespace', () => { |
| let element; |
| let layer; |
| const lineNumberEl = document.createElement('td'); |
| |
| setup(() => { |
| element = basicFixture.instantiate(); |
| element._showTrailingWhitespace = true; |
| layer = element._createTrailingWhitespaceLayer(); |
| }); |
| |
| test('does nothing with empty line', () => { |
| const line = {text: ''}; |
| const el = document.createElement('div'); |
| const annotateElementStub = |
| sinon.stub(GrAnnotation, 'annotateElement'); |
| layer.annotate(el, lineNumberEl, line); |
| assert.isFalse(annotateElementStub.called); |
| }); |
| |
| test('does nothing with no trailing whitespace', () => { |
| const str = 'lorem ipsum blah blah'; |
| const line = {text: str}; |
| const el = document.createElement('div'); |
| el.textContent = str; |
| const annotateElementStub = |
| sinon.stub(GrAnnotation, 'annotateElement'); |
| layer.annotate(el, lineNumberEl, line); |
| assert.isFalse(annotateElementStub.called); |
| }); |
| |
| test('annotates trailing spaces', () => { |
| const str = 'lorem ipsum '; |
| const line = {text: str}; |
| const el = document.createElement('div'); |
| el.textContent = str; |
| const annotateElementStub = |
| sinon.stub(GrAnnotation, 'annotateElement'); |
| layer.annotate(el, lineNumberEl, line); |
| assert.isTrue(annotateElementStub.called); |
| assert.equal(annotateElementStub.lastCall.args[1], 11); |
| assert.equal(annotateElementStub.lastCall.args[2], 3); |
| }); |
| |
| test('annotates trailing tabs', () => { |
| const str = 'lorem ipsum\t\t\t'; |
| const line = {text: str}; |
| const el = document.createElement('div'); |
| el.textContent = str; |
| const annotateElementStub = |
| sinon.stub(GrAnnotation, 'annotateElement'); |
| layer.annotate(el, lineNumberEl, line); |
| assert.isTrue(annotateElementStub.called); |
| assert.equal(annotateElementStub.lastCall.args[1], 11); |
| assert.equal(annotateElementStub.lastCall.args[2], 3); |
| }); |
| |
| test('annotates mixed trailing whitespace', () => { |
| const str = 'lorem ipsum\t \t'; |
| const line = {text: str}; |
| const el = document.createElement('div'); |
| el.textContent = str; |
| const annotateElementStub = |
| sinon.stub(GrAnnotation, 'annotateElement'); |
| layer.annotate(el, lineNumberEl, line); |
| assert.isTrue(annotateElementStub.called); |
| assert.equal(annotateElementStub.lastCall.args[1], 11); |
| assert.equal(annotateElementStub.lastCall.args[2], 3); |
| }); |
| |
| test('unicode preceding trailing whitespace', () => { |
| const str = '💢\t'; |
| const line = {text: str}; |
| const el = document.createElement('div'); |
| el.textContent = str; |
| const annotateElementStub = |
| sinon.stub(GrAnnotation, 'annotateElement'); |
| layer.annotate(el, lineNumberEl, line); |
| assert.isTrue(annotateElementStub.called); |
| assert.equal(annotateElementStub.lastCall.args[1], 1); |
| assert.equal(annotateElementStub.lastCall.args[2], 1); |
| }); |
| |
| test('does not annotate when disabled', () => { |
| element._showTrailingWhitespace = false; |
| const str = 'lorem upsum\t \t '; |
| const line = {text: str}; |
| const el = document.createElement('div'); |
| el.textContent = str; |
| const annotateElementStub = |
| sinon.stub(GrAnnotation, 'annotateElement'); |
| layer.annotate(el, lineNumberEl, line); |
| assert.isFalse(annotateElementStub.called); |
| }); |
| }); |
| |
| suite('rendering text, images and binary files', () => { |
| let processStub; |
| let keyLocations; |
| let prefs; |
| let content; |
| |
| setup(() => { |
| element = basicFixture.instantiate(); |
| element.viewMode = 'SIDE_BY_SIDE'; |
| processStub = sinon.stub(element.$.processor, 'process') |
| .returns(Promise.resolve()); |
| keyLocations = {left: {}, right: {}}; |
| prefs = { |
| line_length: 10, |
| show_tabs: true, |
| tab_size: 4, |
| context: -1, |
| syntax_highlighting: true, |
| }; |
| content = [{ |
| a: ['all work and no play make andybons a dull boy'], |
| b: ['elgoog elgoog elgoog'], |
| }, { |
| ab: [ |
| 'Non eram nescius, Brute, cum, quae summis ingeniis ', |
| 'exquisitaque doctrina philosophi Graeco sermone tractavissent', |
| ], |
| }]; |
| }); |
| |
| test('text', () => { |
| element.diff = {content}; |
| return element.render(keyLocations, prefs).then(() => { |
| assert.isTrue(processStub.calledOnce); |
| assert.isFalse(processStub.lastCall.args[1]); |
| }); |
| }); |
| |
| test('image', () => { |
| element.diff = {content, binary: true}; |
| element.isImageDiff = true; |
| return element.render(keyLocations, prefs).then(() => { |
| assert.isTrue(processStub.calledOnce); |
| assert.isTrue(processStub.lastCall.args[1]); |
| }); |
| }); |
| |
| test('binary', () => { |
| element.diff = {content, binary: true}; |
| return element.render(keyLocations, prefs).then(() => { |
| assert.isTrue(processStub.calledOnce); |
| assert.isTrue(processStub.lastCall.args[1]); |
| }); |
| }); |
| }); |
| |
| suite('rendering', () => { |
| let content; |
| let outputEl; |
| let keyLocations; |
| |
| setup(done => { |
| const prefs = { |
| line_length: 10, |
| show_tabs: true, |
| tab_size: 4, |
| context: -1, |
| syntax_highlighting: true, |
| }; |
| content = [ |
| { |
| a: ['all work and no play make andybons a dull boy'], |
| b: ['elgoog elgoog elgoog'], |
| }, |
| { |
| ab: [ |
| 'Non eram nescius, Brute, cum, quae summis ingeniis ', |
| 'exquisitaque doctrina philosophi Graeco sermone tractavissent', |
| ], |
| }, |
| ]; |
| element = basicFixture.instantiate(); |
| outputEl = element.querySelector('#diffTable'); |
| keyLocations = {left: {}, right: {}}; |
| sinon.stub(element, '_getDiffBuilder').callsFake(() => { |
| const builder = new GrDiffBuilderSideBySide({content}, prefs, outputEl); |
| sinon.stub(builder, 'addColumns'); |
| builder.buildSectionElement = function(group) { |
| const section = document.createElement('stub'); |
| section.textContent = group.lines |
| .reduce((acc, line) => acc + line.text, ''); |
| return section; |
| }; |
| return builder; |
| }); |
| element.diff = {content}; |
| element.render(keyLocations, prefs).then(done); |
| }); |
| |
| test('addColumns is called', done => { |
| element.render(keyLocations, {}).then(done); |
| assert.isTrue(element._builder.addColumns.called); |
| }); |
| |
| test('getSectionsByLineRange one line', () => { |
| const section = outputEl.querySelector('stub:nth-of-type(3)'); |
| const sections = element._builder.getSectionsByLineRange(1, 1, 'left'); |
| assert.equal(sections.length, 1); |
| assert.strictEqual(sections[0], section); |
| }); |
| |
| test('getSectionsByLineRange over diff', () => { |
| const section = [ |
| outputEl.querySelector('stub:nth-of-type(3)'), |
| outputEl.querySelector('stub:nth-of-type(4)'), |
| ]; |
| const sections = element._builder.getSectionsByLineRange(1, 2, 'left'); |
| assert.equal(sections.length, 2); |
| assert.strictEqual(sections[0], section[0]); |
| assert.strictEqual(sections[1], section[1]); |
| }); |
| |
| test('render-start and render-content are fired', done => { |
| const dispatchEventStub = sinon.stub(element, 'dispatchEvent'); |
| element.render(keyLocations, {}).then(() => { |
| const firedEventTypes = dispatchEventStub.getCalls() |
| .map(c => c.args[0].type); |
| assert.include(firedEventTypes, 'render-start'); |
| assert.include(firedEventTypes, 'render-content'); |
| done(); |
| }); |
| }); |
| |
| test('cancel', () => { |
| const processorCancelStub = sinon.stub(element.$.processor, 'cancel'); |
| element.cancel(); |
| assert.isTrue(processorCancelStub.called); |
| }); |
| }); |
| |
| suite('mock-diff', () => { |
| let element; |
| let builder; |
| let diff; |
| let prefs; |
| let keyLocations; |
| |
| setup(done => { |
| element = mockDiffFixture.instantiate(); |
| diff = getMockDiffResponse(); |
| element.diff = diff; |
| |
| prefs = { |
| line_length: 80, |
| show_tabs: true, |
| tab_size: 4, |
| }; |
| keyLocations = {left: {}, right: {}}; |
| |
| element.render(keyLocations, prefs).then(() => { |
| builder = element._builder; |
| done(); |
| }); |
| }); |
| |
| test('aria-labels on added line numbers', () => { |
| const deltaLineNumberButton = element.diffElement.querySelectorAll( |
| '.lineNumButton.right')[5]; |
| |
| assert.isOk(deltaLineNumberButton); |
| assert.equal(deltaLineNumberButton.getAttribute('aria-label'), '5 added'); |
| }); |
| |
| test('aria-labels on removed line numbers', () => { |
| const deltaLineNumberButton = element.diffElement.querySelectorAll( |
| '.lineNumButton.left')[10]; |
| |
| assert.isOk(deltaLineNumberButton); |
| assert.equal( |
| deltaLineNumberButton.getAttribute('aria-label'), '10 removed'); |
| }); |
| |
| test('getContentByLine', () => { |
| let actual; |
| |
| actual = builder.getContentByLine(2, 'left'); |
| assert.equal(actual.textContent, diff.content[0].ab[1]); |
| |
| actual = builder.getContentByLine(2, 'right'); |
| assert.equal(actual.textContent, diff.content[0].ab[1]); |
| |
| actual = builder.getContentByLine(5, 'left'); |
| assert.equal(actual.textContent, diff.content[2].ab[0]); |
| |
| actual = builder.getContentByLine(5, 'right'); |
| assert.equal(actual.textContent, diff.content[1].b[0]); |
| }); |
| |
| test('getContentTdByLineEl works both with button and td', () => { |
| const diffRow = element.diffElement.querySelectorAll('tr.diff-row')[2]; |
| |
| const lineNumTdLeft = diffRow.querySelector('td.lineNum.left'); |
| const lineNumButtonLeft = lineNumTdLeft.querySelector('button'); |
| const contentTdLeft = diffRow.querySelectorAll('.content')[0]; |
| |
| const lineNumTdRight = diffRow.querySelector('td.lineNum.right'); |
| const lineNumButtonRight = lineNumTdRight.querySelector('button'); |
| const contentTdRight = diffRow.querySelectorAll('.content')[1]; |
| |
| assert.equal(element.getContentTdByLineEl(lineNumTdLeft), contentTdLeft); |
| assert.equal( |
| element.getContentTdByLineEl(lineNumButtonLeft), contentTdLeft); |
| assert.equal( |
| element.getContentTdByLineEl(lineNumTdRight), contentTdRight); |
| assert.equal( |
| element.getContentTdByLineEl(lineNumButtonRight), contentTdRight); |
| }); |
| |
| test('findLinesByRange', () => { |
| const lines = []; |
| const elems = []; |
| const start = 6; |
| const end = 10; |
| const count = end - start + 1; |
| |
| builder.findLinesByRange(start, end, 'right', lines, elems); |
| |
| assert.equal(lines.length, count); |
| assert.equal(elems.length, count); |
| |
| for (let i = 0; i < 5; i++) { |
| assert.instanceOf(lines[i], GrDiffLine); |
| assert.equal(lines[i].afterNumber, start + i); |
| assert.instanceOf(elems[i], HTMLElement); |
| assert.equal(lines[i].text, elems[i].textContent); |
| } |
| }); |
| |
| test('_renderContentByRange', () => { |
| const spy = sinon.spy(builder, '_createTextEl'); |
| const start = 9; |
| const end = 14; |
| const count = end - start + 1; |
| |
| builder._renderContentByRange(start, end, 'left'); |
| |
| assert.equal(spy.callCount, count); |
| spy.getCalls().forEach((call, i) => { |
| assert.equal(call.args[1].beforeNumber, start + i); |
| }); |
| }); |
| |
| test('_renderContentByRange notexistent elements', () => { |
| const spy = sinon.spy(builder, '_createTextEl'); |
| |
| sinon.stub(builder, '_getLineNumberEl').returns( |
| document.createElement('div') |
| ); |
| sinon.stub(builder, 'findLinesByRange').callsFake( |
| (s, e, d, lines, elements) => { |
| // Add a line and a corresponding element. |
| lines.push(new GrDiffLine(GrDiffLineType.BOTH)); |
| const tr = document.createElement('tr'); |
| const td = document.createElement('td'); |
| const el = document.createElement('div'); |
| tr.appendChild(td); |
| td.appendChild(el); |
| elements.push(el); |
| |
| // Add 2 lines without corresponding elements. |
| lines.push(new GrDiffLine(GrDiffLineType.BOTH)); |
| lines.push(new GrDiffLine(GrDiffLineType.BOTH)); |
| }); |
| |
| builder._renderContentByRange(1, 10, 'left'); |
| // Should be called only once because only one line had a corresponding |
| // element. |
| assert.equal(spy.callCount, 1); |
| }); |
| |
| test('_getLineNumberEl side-by-side left', () => { |
| const contentEl = builder.getContentByLine(5, 'left', |
| element.$.diffTable); |
| const lineNumberEl = builder._getLineNumberEl(contentEl, 'left'); |
| assert.isTrue(lineNumberEl.classList.contains('lineNum')); |
| assert.isTrue(lineNumberEl.classList.contains('left')); |
| }); |
| |
| test('_getLineNumberEl side-by-side right', () => { |
| const contentEl = builder.getContentByLine(5, 'right', |
| element.$.diffTable); |
| const lineNumberEl = builder._getLineNumberEl(contentEl, 'right'); |
| assert.isTrue(lineNumberEl.classList.contains('lineNum')); |
| assert.isTrue(lineNumberEl.classList.contains('right')); |
| }); |
| |
| test('_getLineNumberEl unified left', done => { |
| // Re-render as unified: |
| element.viewMode = 'UNIFIED_DIFF'; |
| element.render(keyLocations, prefs).then(() => { |
| builder = element._builder; |
| |
| const contentEl = builder.getContentByLine(5, 'left', |
| element.$.diffTable); |
| const lineNumberEl = builder._getLineNumberEl(contentEl, 'left'); |
| assert.isTrue(lineNumberEl.classList.contains('lineNum')); |
| assert.isTrue(lineNumberEl.classList.contains('left')); |
| done(); |
| }); |
| }); |
| |
| test('_getLineNumberEl unified right', done => { |
| // Re-render as unified: |
| element.viewMode = 'UNIFIED_DIFF'; |
| element.render(keyLocations, prefs).then(() => { |
| builder = element._builder; |
| |
| const contentEl = builder.getContentByLine(5, 'right', |
| element.$.diffTable); |
| const lineNumberEl = builder._getLineNumberEl(contentEl, 'right'); |
| assert.isTrue(lineNumberEl.classList.contains('lineNum')); |
| assert.isTrue(lineNumberEl.classList.contains('right')); |
| done(); |
| }); |
| }); |
| |
| test('_getNextContentOnSide side-by-side left', () => { |
| const startElem = builder.getContentByLine(5, 'left', |
| element.$.diffTable); |
| const expectedStartString = diff.content[2].ab[0]; |
| const expectedNextString = diff.content[2].ab[1]; |
| assert.equal(startElem.textContent, expectedStartString); |
| |
| const nextElem = builder._getNextContentOnSide(startElem, |
| 'left'); |
| assert.equal(nextElem.textContent, expectedNextString); |
| }); |
| |
| test('_getNextContentOnSide side-by-side right', () => { |
| const startElem = builder.getContentByLine(5, 'right', |
| element.$.diffTable); |
| const expectedStartString = diff.content[1].b[0]; |
| const expectedNextString = diff.content[1].b[1]; |
| assert.equal(startElem.textContent, expectedStartString); |
| |
| const nextElem = builder._getNextContentOnSide(startElem, |
| 'right'); |
| assert.equal(nextElem.textContent, expectedNextString); |
| }); |
| |
| test('_getNextContentOnSide unified left', done => { |
| // Re-render as unified: |
| element.viewMode = 'UNIFIED_DIFF'; |
| element.render(keyLocations, prefs).then(() => { |
| builder = element._builder; |
| |
| const startElem = builder.getContentByLine(5, 'left', |
| element.$.diffTable); |
| const expectedStartString = diff.content[2].ab[0]; |
| const expectedNextString = diff.content[2].ab[1]; |
| assert.equal(startElem.textContent, expectedStartString); |
| |
| const nextElem = builder._getNextContentOnSide(startElem, |
| 'left'); |
| assert.equal(nextElem.textContent, expectedNextString); |
| |
| done(); |
| }); |
| }); |
| |
| test('_getNextContentOnSide unified right', done => { |
| // Re-render as unified: |
| element.viewMode = 'UNIFIED_DIFF'; |
| element.render(keyLocations, prefs).then(() => { |
| builder = element._builder; |
| |
| const startElem = builder.getContentByLine(5, 'right', |
| element.$.diffTable); |
| const expectedStartString = diff.content[1].b[0]; |
| const expectedNextString = diff.content[1].b[1]; |
| assert.equal(startElem.textContent, expectedStartString); |
| |
| const nextElem = builder._getNextContentOnSide(startElem, |
| 'right'); |
| assert.equal(nextElem.textContent, expectedNextString); |
| |
| done(); |
| }); |
| }); |
| |
| test('escaping HTML', () => { |
| let input = '<script>alert("XSS");<' + '/script>'; |
| let expected = '<script>alert("XSS");</script>'; |
| let result = builder._formatText(input, 1, Infinity).innerHTML; |
| assert.equal(result, expected); |
| |
| input = '& < > " \' / `'; |
| expected = '& < > " \' / `'; |
| result = builder._formatText(input, 1, Infinity).innerHTML; |
| assert.equal(result, expected); |
| }); |
| }); |
| |
| suite('blame', () => { |
| let mockBlame; |
| |
| setup(() => { |
| mockBlame = [ |
| {id: 'commit 1', ranges: [{start: 1, end: 2}, {start: 10, end: 16}]}, |
| {id: 'commit 2', ranges: [{start: 4, end: 10}, {start: 17, end: 32}]}, |
| ]; |
| }); |
| |
| test('setBlame attempts to render each blamed line', () => { |
| const getBlameStub = sinon.stub(builder, '_getBlameByLineNum') |
| .returns(null); |
| builder.setBlame(mockBlame); |
| assert.equal(getBlameStub.callCount, 32); |
| }); |
| |
| test('_getBlameCommitForBaseLine', () => { |
| sinon.stub(builder, '_getBlameByLineNum').returns(null); |
| builder.setBlame(mockBlame); |
| assert.isOk(builder._getBlameCommitForBaseLine(1)); |
| assert.equal(builder._getBlameCommitForBaseLine(1).id, 'commit 1'); |
| |
| assert.isOk(builder._getBlameCommitForBaseLine(11)); |
| assert.equal(builder._getBlameCommitForBaseLine(11).id, 'commit 1'); |
| |
| assert.isOk(builder._getBlameCommitForBaseLine(32)); |
| assert.equal(builder._getBlameCommitForBaseLine(32).id, 'commit 2'); |
| |
| assert.isNull(builder._getBlameCommitForBaseLine(33)); |
| }); |
| |
| test('_getBlameCommitForBaseLine w/o blame returns null', () => { |
| assert.isNull(builder._getBlameCommitForBaseLine(1)); |
| assert.isNull(builder._getBlameCommitForBaseLine(11)); |
| assert.isNull(builder._getBlameCommitForBaseLine(31)); |
| }); |
| |
| test('_createBlameCell', () => { |
| const mocbBlameCell = document.createElement('span'); |
| const getBlameStub = sinon.stub(builder, '_getBlameForBaseLine') |
| .returns(mocbBlameCell); |
| const line = new GrDiffLine(GrDiffLineType.BOTH); |
| line.beforeNumber = 3; |
| line.afterNumber = 5; |
| |
| const result = builder._createBlameCell(line.beforeNumber); |
| |
| assert.isTrue(getBlameStub.calledWithExactly(3)); |
| assert.equal(result.getAttribute('data-line-number'), '3'); |
| assert.equal(result.firstChild, mocbBlameCell); |
| }); |
| |
| test('_getBlameForBaseLine', () => { |
| const mockCommit = { |
| time: 1576105200, |
| id: 1234567890, |
| author: 'Clark Kent', |
| commit_msg: 'Testing Commit', |
| ranges: [1], |
| }; |
| const blameNode = builder._getBlameForBaseLine(1, mockCommit); |
| |
| const authors = blameNode.getElementsByClassName('blameAuthor'); |
| assert.equal(authors.length, 1); |
| assert.equal(authors[0].innerText, ' Clark'); |
| |
| const date = (new Date(mockCommit.time * 1000)).toLocaleDateString(); |
| flush(); |
| const cards = blameNode.getElementsByClassName('blameHoverCard'); |
| assert.equal(cards.length, 1); |
| assert.equal(cards[0].innerHTML, |
| `Commit 1234567890<br>Author: Clark Kent<br>Date: ${date}` |
| + '<br><br>Testing Commit' |
| ); |
| |
| const url = blameNode.getElementsByClassName('blameDate'); |
| assert.equal(url[0].getAttribute('href'), '/r/q/1234567890'); |
| }); |
| }); |
| }); |
| |