diff options
Diffstat (limited to 'polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element_test.js')
-rw-r--r-- | polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element_test.js | 1084 |
1 files changed, 1084 insertions, 0 deletions
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element_test.js b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element_test.js new file mode 100644 index 0000000000..0ad21b0a6a --- /dev/null +++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element_test.js @@ -0,0 +1,1084 @@ +/** + * @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 {createDiff} from '../../../test/test-data-generators.js'; +import './gr-diff-builder-element.js'; +import {stubBaseUrl} from '../../../test/test-utils.js'; +import {GrAnnotation} from '../gr-diff-highlight/gr-annotation.js'; +import {GrDiffLine, GrDiffLineType} from '../gr-diff/gr-diff-line.js'; +import {GrDiffBuilderSideBySide} from './gr-diff-builder-side-by-side.js'; +import {html} from '@polymer/polymer/lib/utils/html-tag.js'; +import {DiffViewMode, Side} from '../../../api/diff.js'; +import {stubRestApi} from '../../../test/test-utils.js'; +import {afterNextRender} from '@polymer/polymer/lib/utils/render-status'; +import {GrDiffBuilderLegacy} from './gr-diff-builder-legacy.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> +`); + +// GrDiffBuilderElement forces these prefs to be set - tests that do not care +// about these values can just set these defaults. +const DEFAULT_PREFS = { + line_length: 10, + show_tabs: true, + tab_size: 4, +}; + +suite('gr-diff-builder tests', () => { + let prefs; + let element; + let builder; + + const LINE_BREAK_HTML = '<span class="style-scope gr-diff br"></span>'; + const WBR_HTML = '<wbr class="style-scope gr-diff">'; + + setup(() => { + element = basicFixture.instantiate(); + stubRestApi('getLoggedIn').returns(Promise.resolve(false)); + stubRestApi('getProjectConfig').returns(Promise.resolve({})); + stubBaseUrl('/r'); + prefs = {...DEFAULT_PREFS}; + builder = new GrDiffBuilderLegacy({content: []}, prefs); + }); + + test('line_length applied with <wbr> 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 expected = 'a'.repeat(50) + WBR_HTML + 'a'; + const result = builder.createTextEl(undefined, line).firstChild.innerHTML; + assert.equal(result, expected); + }); + + test('line_length applied with line break 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_BREAK_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; + element.diff = {}; + element.prefs = {tab_size: 4, line_length: 50}; + builder = element._getDiffBuilder(); + assert.equal(builder._prefs.line_length, 50); + }); + + test(`line_length ignored for commit msg under ${mode}`, () => { + element.path = '/COMMIT_MSG'; + element.viewMode = mode; + element.diff = {}; + element.prefs = {tab_size: 4, line_length: 50}; + builder = element._getDiffBuilder(); + 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('_handlePreferenceError throws with invalid preference', () => { + element.prefs = {tab_size: 0}; + assert.throws(() => element._getDiffBuilder()); + }); + + 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('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 content; + + setup(() => { + element = basicFixture.instantiate(); + element.viewMode = 'SIDE_BY_SIDE'; + processStub = sinon.stub(element.processor, 'process') + .returns(Promise.resolve()); + keyLocations = {left: {}, right: {}}; + element.prefs = { + ...DEFAULT_PREFS, + 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).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).then(() => { + assert.isTrue(processStub.calledOnce); + assert.isTrue(processStub.lastCall.args[1]); + }); + }); + + test('binary', () => { + element.diff = {content, binary: true}; + return element.render(keyLocations).then(() => { + assert.isTrue(processStub.calledOnce); + assert.isTrue(processStub.lastCall.args[1]); + }); + }); + }); + + suite('rendering', () => { + let content; + let outputEl; + let keyLocations; + + setup(async () => { + const prefs = {...DEFAULT_PREFS}; + 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(); + sinon.stub(element, 'dispatchEvent'); + 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.prefs = prefs; + await element.render(keyLocations); + }); + + test('addColumns is called', () => { + assert.isTrue(element._builder.addColumns.called); + }); + + test('getGroupsByLineRange one line', () => { + const section = outputEl.querySelector('stub:nth-of-type(3)'); + const groups = element._builder.getGroupsByLineRange(1, 1, 'left'); + assert.equal(groups.length, 1); + assert.strictEqual(groups[0].element, section); + }); + + test('getGroupsByLineRange over diff', () => { + const section = [ + outputEl.querySelector('stub:nth-of-type(3)'), + outputEl.querySelector('stub:nth-of-type(4)'), + ]; + const groups = element._builder.getGroupsByLineRange(1, 2, 'left'); + assert.equal(groups.length, 2); + assert.strictEqual(groups[0].element, section[0]); + assert.strictEqual(groups[1].element, section[1]); + }); + + test('render-start and render-content are fired', async () => { + const firedEventTypes = element.dispatchEvent.getCalls() + .map(c => c.args[0].type); + assert.include(firedEventTypes, 'render-start'); + assert.include(firedEventTypes, 'render-content'); + }); + + test('cancel cancels the processor', () => { + const processorCancelStub = sinon.stub(element.processor, 'cancel'); + element.cancel(); + assert.isTrue(processorCancelStub.called); + }); + }); + + suite('context hiding and expanding', () => { + setup(async () => { + element = basicFixture.instantiate(); + sinon.stub(element, 'dispatchEvent'); + const afterNextRenderPromise = new Promise((resolve, reject) => { + afterNextRender(element, resolve); + }); + element.diff = { + content: [ + {ab: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(i => `unchanged ${i}`)}, + {a: ['before'], b: ['after']}, + {ab: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(i => `unchanged ${10 + i}`)}, + ], + }; + element.viewMode = DiffViewMode.SIDE_BY_SIDE; + + const keyLocations = {left: {}, right: {}}; + element.prefs = { + ...DEFAULT_PREFS, + context: 1, + }; + await element.render(keyLocations); + // Make sure all listeners are installed. + await afterNextRenderPromise; + }); + + test('hides lines behind two context controls', () => { + const contextControls = element.querySelectorAll('gr-context-controls'); + assert.equal(contextControls.length, 2); + + const diffRows = element.querySelectorAll('.diff-row'); + // The first two are LOST and FILE line + assert.equal(diffRows.length, 2 + 1 + 1 + 1); + assert.include(diffRows[2].textContent, 'unchanged 10'); + assert.include(diffRows[3].textContent, 'before'); + assert.include(diffRows[3].textContent, 'after'); + assert.include(diffRows[4].textContent, 'unchanged 11'); + }); + + test('clicking +x common lines expands those lines', () => { + const contextControls = element.querySelectorAll('gr-context-controls'); + const topExpandCommonButton = contextControls[0].shadowRoot + .querySelectorAll('.showContext')[0]; + assert.include(topExpandCommonButton.textContent, '+9 common lines'); + topExpandCommonButton.click(); + const diffRows = element.querySelectorAll('.diff-row'); + // The first two are LOST and FILE line + assert.equal(diffRows.length, 2 + 10 + 1 + 1); + assert.include(diffRows[2].textContent, 'unchanged 1'); + assert.include(diffRows[3].textContent, 'unchanged 2'); + assert.include(diffRows[4].textContent, 'unchanged 3'); + assert.include(diffRows[5].textContent, 'unchanged 4'); + assert.include(diffRows[6].textContent, 'unchanged 5'); + assert.include(diffRows[7].textContent, 'unchanged 6'); + assert.include(diffRows[8].textContent, 'unchanged 7'); + assert.include(diffRows[9].textContent, 'unchanged 8'); + assert.include(diffRows[10].textContent, 'unchanged 9'); + assert.include(diffRows[11].textContent, 'unchanged 10'); + assert.include(diffRows[12].textContent, 'before'); + assert.include(diffRows[12].textContent, 'after'); + assert.include(diffRows[13].textContent, 'unchanged 11'); + }); + + test('unhideLine shows the line with context', async () => { + const clock = sinon.useFakeTimers(); + element.dispatchEvent.reset(); + element.unhideLine(4, Side.LEFT); + + const diffRows = element.querySelectorAll('.diff-row'); + // The first two are LOST and FILE line + // Lines 3-5 (Line 4 plus 1 context in each direction) will be expanded + // Because context expanders do not hide <3 lines, lines 1-2 will also + // be shown. + // Lines 6-9 continue to be hidden + assert.equal(diffRows.length, 2 + 5 + 1 + 1 + 1); + assert.include(diffRows[2].textContent, 'unchanged 1'); + assert.include(diffRows[3].textContent, 'unchanged 2'); + assert.include(diffRows[4].textContent, 'unchanged 3'); + assert.include(diffRows[5].textContent, 'unchanged 4'); + assert.include(diffRows[6].textContent, 'unchanged 5'); + assert.include(diffRows[7].textContent, 'unchanged 10'); + assert.include(diffRows[8].textContent, 'before'); + assert.include(diffRows[8].textContent, 'after'); + assert.include(diffRows[9].textContent, 'unchanged 11'); + + clock.tick(1); + await flush(); + const firedEventTypes = element.dispatchEvent.getCalls() + .map(c => c.args[0].type); + assert.include(firedEventTypes, 'render-content'); + }); + }); + + suite('mock-diff', () => { + let element; + let builder; + let diff; + let keyLocations; + + setup(async () => { + element = mockDiffFixture.instantiate(); + diff = createDiff(); + element.diff = diff; + + keyLocations = {left: {}, right: {}}; + + element.prefs = { + line_length: 80, + show_tabs: true, + tab_size: 4, + }; + await element.render(keyLocations); + builder = element._builder; + }); + + 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 non-existent 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', async () => { + // Re-render as unified: + element.viewMode = 'UNIFIED_DIFF'; + await element.render(keyLocations); + 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')); + }); + + test('getLineNumberEl unified right', async () => { + // Re-render as unified: + element.viewMode = 'UNIFIED_DIFF'; + await element.render(keyLocations); + 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')); + }); + + 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', async () => { + // Re-render as unified: + element.viewMode = 'UNIFIED_DIFF'; + await element.render(keyLocations); + 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); + }); + + test('getNextContentOnSide unified right', async () => { + // Re-render as unified: + element.viewMode = 'UNIFIED_DIFF'; + await element.render(keyLocations); + 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); + }); + }); + + 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, 'getBlameTdByLine') + .returns(null); + builder.setBlame(mockBlame); + assert.equal(getBlameStub.callCount, 32); + }); + + test('getBlameCommitForBaseLine', () => { + sinon.stub(builder, 'getBlameTdByLine').returns(undefined); + 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.isUndefined(builder.getBlameCommitForBaseLine(33)); + }); + + test('getBlameCommitForBaseLine w/o blame returns null', () => { + assert.isUndefined(builder.getBlameCommitForBaseLine(1)); + assert.isUndefined(builder.getBlameCommitForBaseLine(11)); + assert.isUndefined(builder.getBlameCommitForBaseLine(31)); + }); + + test('createBlameCell', () => { + const mockBlameInfo = { + time: 1576155200, + id: 1234567890, + author: 'Clark Kent', + commit_msg: 'Testing Commit', + ranges: [1], + }; + const getBlameStub = sinon.stub(builder, 'getBlameCommitForBaseLine') + .returns(mockBlameInfo); + 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'); + expect(result).dom.to.equal(/* HTML */` + <span class="gr-diff style-scope"> + <a class="blameDate gr-diff style-scope" href="/r/q/1234567890"> + 12/12/2019 + </a> + <span class="blameAuthor gr-diff style-scope">Clark</span> + <gr-hovercard class="gr-diff style-scope"> + <span class="blameHoverCard gr-diff style-scope"> + Commit 1234567890<br> + Author: Clark Kent<br> + Date: 12/12/2019<br> + <br> + Testing Commit + </span> + </gr-hovercard> + </span> + `); + }); + }); +}); + |