/** * @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 '../../shared/gr-rest-api-interface/gr-rest-api-interface.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'; const basicFixture = fixtureFromTemplate(html`
`); const divWithTextFixture = fixtureFromTemplate(html`
Lorem ipsum dolor sit amet, suspendisse inceptos vehicula
`); const mockDiffFixture = fixtureFromTemplate(html`
`); const DiffViewMode = { SIDE_BY_SIDE: 'SIDE_BY_SIDE', UNIFIED: 'UNIFIED_DIFF', }; suite('gr-diff-builder tests', () => { let prefs; let element; let builder; const LINE_FEED_HTML = ''; setup(() => { element = basicFixture.instantiate(); stub('gr-rest-api-interface', { getLoggedIn() { return Promise.resolve(false); }, getProjectConfig() { return 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)]; } test('no +10 buttons for 10 or less lines', () => { const contextGroups = createContextGroups({count: 10}); const td = builder._createContextControl({}, contextGroups); const buttons = td.querySelectorAll('gr-button.showContext'); assert.equal(buttons.length, 1); assert.equal(buttons[0].textContent, 'Show 10 common lines'); }); test('context control at the top', () => { const contextGroups = createContextGroups({offset: 0, count: 20}); builder._numLinesLeft = 50; const td = builder._createContextControl({}, contextGroups); const buttons = td.querySelectorAll('gr-button.showContext'); assert.equal(buttons.length, 2); assert.equal(buttons[0].textContent, 'Show 20 common lines'); assert.equal(buttons[1].textContent, '+10 below'); }); test('context control in the middle', () => { const contextGroups = createContextGroups({offset: 10, count: 20}); builder._numLinesLeft = 50; const td = builder._createContextControl({}, contextGroups); const buttons = td.querySelectorAll('gr-button.showContext'); assert.equal(buttons.length, 3); assert.equal(buttons[0].textContent, '+10 above'); assert.equal(buttons[1].textContent, 'Show 20 common lines'); assert.equal(buttons[2].textContent, '+10 below'); }); test('context control at the top', () => { const contextGroups = createContextGroups({offset: 30, count: 20}); builder._numLinesLeft = 50; const td = builder._createContextControl({}, contextGroups); const buttons = td.querySelectorAll('gr-button.showContext'); assert.equal(buttons.length, 2); assert.equal(buttons[0].textContent, '+10 above'); assert.equal(buttons[1].textContent, 'Show 20 common lines'); }); }); 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 = '๐Ÿ‘'; 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('^\\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.queryEffectiveChildren('#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(2)'); 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(2)'), outputEl.querySelector('stub:nth-of-type(3)'), ]; 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 = '