Migrate gr-diff-builder-element_test from js to ts
Release-Notes: skip
Change-Id: Ieae0bfb7ca9c13a51f3aa07fa89f123450396e57
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element.ts
index 269b56d..efd6f10 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element.ts
@@ -64,7 +64,21 @@
declare global {
interface HTMLElementEventMap {
+ /**
+ * Fired when the diff begins rendering - both for full renders and for
+ * partial rerenders.
+ */
+ 'render-start': CustomEvent<{}>;
+ /**
+ * Fired whenever a new chunk of lines has been rendered synchronously - this
+ * only happens for full renders.
+ */
'render-progress': CustomEvent<RenderProgressEventDetail>;
+ /**
+ * Fired when the diff finishes rendering text content - both for full
+ * renders and for partial rerenders.
+ */
+ 'render-content': CustomEvent<{}>;
}
}
@@ -106,27 +120,6 @@
return htmlTemplate;
}
- /**
- * Fired when the diff begins rendering - both for full renders and for
- * partial rerenders.
- *
- * @event render-start
- */
-
- /**
- * Fired whenever a new chunk of lines has been rendered synchronously - this
- * only happens for full renders.
- *
- * @event render-progress
- */
-
- /**
- * Fired when the diff finishes rendering text content - both for full
- * renders and for partial rerenders.
- *
- * @event render-content
- */
-
@property({type: Object})
diff?: DiffInfo;
@@ -210,7 +203,8 @@
private rangeLayer = new GrRangedCommentLayer();
- private processor = new GrDiffProcessor();
+ // visible for testing
+ processor = new GrDiffProcessor();
constructor() {
super();
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
deleted file mode 100644
index 8c15ddd..0000000
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element_test.js
+++ /dev/null
@@ -1,1084 +0,0 @@
-/**
- * @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';
-import {waitForEventOnce} from '../../../utils/event-util.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', async () => {
- element.diff = {content};
- element.render(keyLocations);
- await waitForEventOnce(element, 'render-content');
- assert.isTrue(processStub.calledOnce);
- assert.isFalse(processStub.lastCall.args[1]);
- });
-
- test('image', async () => {
- element.diff = {content, binary: true};
- element.isImageDiff = true;
- element.render(keyLocations);
- await waitForEventOnce(element, 'render-content');
- assert.isTrue(processStub.calledOnce);
- assert.isTrue(processStub.lastCall.args[1]);
- });
-
- test('binary', async () => {
- element.diff = {content, binary: true};
- element.render(keyLocations);
- await waitForEventOnce(element, 'render-content');
- 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 () => {
- await new Promise(resolve => afterNextRender(element, resolve));
- 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 () => {
- 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');
-
- await new Promise(resolve => afterNextRender(element, resolve));
- 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>
- `);
- });
- });
-});
-
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element_test.ts
new file mode 100644
index 0000000..b441ad3
--- /dev/null
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element_test.ts
@@ -0,0 +1,1149 @@
+/**
+ * @license
+ * Copyright 2016 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup-karma';
+import {
+ createConfig,
+ createDiff,
+ createEmptyDiff,
+} from '../../../test/test-data-generators';
+import './gr-diff-builder-element';
+import {queryAndAssert, stubBaseUrl} from '../../../test/test-utils';
+import {GrAnnotation} from '../gr-diff-highlight/gr-annotation';
+import {GrDiffLine, GrDiffLineType} from '../gr-diff/gr-diff-line';
+import {GrDiffBuilderSideBySide} from './gr-diff-builder-side-by-side';
+import {html} from '@polymer/polymer/lib/utils/html-tag';
+import {
+ DiffContent,
+ DiffInfo,
+ DiffLayer,
+ DiffPreferencesInfo,
+ DiffViewMode,
+ Side,
+} from '../../../api/diff';
+import {stubRestApi} from '../../../test/test-utils';
+import {afterNextRender} from '@polymer/polymer/lib/utils/render-status';
+import {GrDiffBuilderLegacy} from './gr-diff-builder-legacy';
+import {waitForEventOnce} from '../../../utils/event-util';
+import {GrDiffBuilderElement} from './gr-diff-builder-element';
+import {createDefaultDiffPrefs} from '../../../constants/constants';
+import {KeyLocations} from '../gr-diff-processor/gr-diff-processor';
+import {BlameInfo} from '../../../types/common';
+
+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>
+`);
+
+const DEFAULT_PREFS = createDefaultDiffPrefs();
+
+suite('gr-diff-builder tests', () => {
+ let element: GrDiffBuilderElement;
+ let builder: GrDiffBuilderLegacy;
+
+ const LINE_BREAK_HTML = '<span class="style-scope gr-diff br"></span>';
+ const WBR_HTML = '<wbr class="style-scope gr-diff">';
+
+ const setBuilderPrefs = (prefs: Partial<DiffPreferencesInfo>) => {
+ builder = new GrDiffBuilderSideBySide(
+ createEmptyDiff(),
+ {...createDefaultDiffPrefs(), ...prefs},
+ element.diffElement
+ );
+ };
+
+ const line = (text: string) => {
+ const line = new GrDiffLine(GrDiffLineType.BOTH);
+ line.text = text;
+ return line;
+ };
+
+ setup(() => {
+ element = basicFixture.instantiate() as GrDiffBuilderElement;
+ stubRestApi('getLoggedIn').returns(Promise.resolve(false));
+ stubRestApi('getProjectConfig').returns(Promise.resolve(createConfig()));
+ stubBaseUrl('/r');
+ setBuilderPrefs({});
+ });
+
+ test('line_length applied with <wbr> if line_wrapping is true', () => {
+ setBuilderPrefs({line_wrapping: true, tab_size: 4, line_length: 50});
+ const text = 'a'.repeat(51);
+ const expected = 'a'.repeat(50) + WBR_HTML + 'a';
+ const result = builder.createTextEl(null, line(text)).firstElementChild
+ ?.innerHTML;
+ assert.equal(result, expected);
+ });
+
+ test('line_length applied with line break if line_wrapping is false', () => {
+ setBuilderPrefs({line_wrapping: false, tab_size: 4, line_length: 50});
+ const text = 'a'.repeat(51);
+ const expected = 'a'.repeat(50) + LINE_BREAK_HTML + 'a';
+ const result = builder.createTextEl(null, line(text)).firstElementChild
+ ?.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 = createEmptyDiff();
+ element.prefs = {
+ ...createDefaultDiffPrefs(),
+ tab_size: 4,
+ line_length: 50,
+ };
+ builder = element._getDiffBuilder() as GrDiffBuilderLegacy;
+ 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 = createEmptyDiff();
+ element.prefs = {
+ ...createDefaultDiffPrefs(),
+ tab_size: 4,
+ line_length: 50,
+ };
+ builder = element._getDiffBuilder() as GrDiffBuilderLegacy;
+ assert.equal(builder._prefs.line_length, 72);
+ });
+ });
+
+ test('createTextEl linewrap with tabs', () => {
+ setBuilderPrefs({tab_size: 4, line_length: 10});
+ const text = '\t'.repeat(7) + '!';
+ const el = builder.createTextEl(null, line(text));
+ assert.equal(el.innerText, text);
+ // With line length 10 and tab size 4, 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 = {...createDefaultDiffPrefs(), 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: HTMLElement;
+ let str: string;
+ let annotateElementSpy: sinon.SinonSpy;
+ let layer: DiffLayer;
+ const lineNumberEl = document.createElement('td');
+
+ function slice(str: string, start: number, end?: number) {
+ return Array.from(str).slice(start, end).join('');
+ }
+
+ setup(() => {
+ el = divWithTextFixture.instantiate() as HTMLElement;
+ str = el.textContent ?? '';
+ annotateElementSpy = sinon.spy(GrAnnotation, 'annotateElement');
+ layer = document.createElement('gr-diff-builder')._createIntralineLayer();
+ });
+
+ test('annotate no highlights', () => {
+ layer.annotate(el, lineNumberEl, line(str), Side.LEFT);
+
+ // 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 l = line(str);
+ l.highlights = [
+ {contentIndex: 0, startIndex: 6, endIndex: 12},
+ {contentIndex: 0, 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, l, Side.LEFT);
+
+ 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 l = line(str);
+ l.highlights = [{contentIndex: 0, startIndex: 28}];
+
+ const str0 = slice(str, 0, 28);
+ const str1 = slice(str, 28);
+
+ layer.annotate(el, lineNumberEl, l, Side.LEFT);
+
+ 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 l = line(str);
+ l.highlights = [{contentIndex: 0, startIndex: 28, endIndex: 28}];
+
+ layer.annotate(el, lineNumberEl, l, Side.LEFT);
+
+ 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 l = line(str);
+ l.highlights = [{contentIndex: 0, 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, l, Side.LEFT);
+
+ 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 l = line(str);
+ l.highlights = [{contentIndex: 0, startIndex: 6}];
+
+ const str0 = slice(str, 0, 6);
+ const str1 = slice(str, 6);
+
+ layer.annotate(el, lineNumberEl, l, Side.LEFT);
+
+ 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: GrDiffBuilderElement;
+ let layer: DiffLayer;
+ const lineNumberEl = document.createElement('td');
+
+ setup(() => {
+ element = basicFixture.instantiate() as GrDiffBuilderElement;
+ element._showTabs = true;
+ layer = element._createTabIndicatorLayer();
+ });
+
+ test('does nothing with empty line', () => {
+ const l = line('');
+ const el = document.createElement('div');
+ const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+
+ layer.annotate(el, lineNumberEl, l, Side.LEFT);
+
+ assert.isFalse(annotateElementStub.called);
+ });
+
+ test('does nothing with no tabs', () => {
+ const str = 'lorem ipsum no tabs';
+ const l = line(str);
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+
+ layer.annotate(el, lineNumberEl, l, Side.LEFT);
+
+ assert.isFalse(annotateElementStub.called);
+ });
+
+ test('annotates tab at beginning', () => {
+ const str = '\tlorem upsum';
+ const l = line(str);
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+
+ layer.annotate(el, lineNumberEl, l, Side.LEFT);
+
+ 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 l = line(str);
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+
+ layer.annotate(el, lineNumberEl, l, Side.LEFT);
+
+ assert.isFalse(annotateElementStub.called);
+ });
+
+ test('annotates multiple in beginning', () => {
+ const str = '\t\tlorem upsum';
+ const l = line(str);
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+
+ layer.annotate(el, lineNumberEl, l, Side.LEFT);
+
+ 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 l = line(str);
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+
+ layer.annotate(el, lineNumberEl, l, Side.LEFT);
+
+ 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: GrDiffBuilderElement;
+ let initialLayersCount = 0;
+ let withLayerCount = 0;
+ setup(() => {
+ const layers: DiffLayer[] = [];
+ element = basicFixture.instantiate() as GrDiffBuilderElement;
+ 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: DiffLayer[] = [{annotate: () => {}}, {annotate: () => {}}];
+ setup(() => {
+ element = basicFixture.instantiate() as GrDiffBuilderElement;
+ 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: GrDiffBuilderElement;
+ let layer: DiffLayer;
+ const lineNumberEl = document.createElement('td');
+
+ setup(() => {
+ element = basicFixture.instantiate() as GrDiffBuilderElement;
+ element._showTrailingWhitespace = true;
+ layer = element._createTrailingWhitespaceLayer();
+ });
+
+ test('does nothing with empty line', () => {
+ const l = line('');
+ const el = document.createElement('div');
+ const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+ layer.annotate(el, lineNumberEl, l, Side.LEFT);
+ assert.isFalse(annotateElementStub.called);
+ });
+
+ test('does nothing with no trailing whitespace', () => {
+ const str = 'lorem ipsum blah blah';
+ const l = line(str);
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+ layer.annotate(el, lineNumberEl, l, Side.LEFT);
+ assert.isFalse(annotateElementStub.called);
+ });
+
+ test('annotates trailing spaces', () => {
+ const str = 'lorem ipsum ';
+ const l = line(str);
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+ layer.annotate(el, lineNumberEl, l, Side.LEFT);
+ 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 l = line(str);
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+ layer.annotate(el, lineNumberEl, l, Side.LEFT);
+ 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 l = line(str);
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+ layer.annotate(el, lineNumberEl, l, Side.LEFT);
+ 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 l = line(str);
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+ layer.annotate(el, lineNumberEl, l, Side.LEFT);
+ 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 l = line(str);
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+ layer.annotate(el, lineNumberEl, l, Side.LEFT);
+ assert.isFalse(annotateElementStub.called);
+ });
+ });
+
+ suite('rendering text, images and binary files', () => {
+ let processStub: sinon.SinonStub;
+ let keyLocations: KeyLocations;
+ let content: DiffContent[] = [];
+
+ setup(() => {
+ element = basicFixture.instantiate() as GrDiffBuilderElement;
+ 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', async () => {
+ element.diff = {...createEmptyDiff(), content};
+ element.render(keyLocations);
+ await waitForEventOnce(element, 'render-content');
+ assert.isTrue(processStub.calledOnce);
+ assert.isFalse(processStub.lastCall.args[1]);
+ });
+
+ test('image', async () => {
+ element.diff = {...createEmptyDiff(), content, binary: true};
+ element.isImageDiff = true;
+ element.render(keyLocations);
+ await waitForEventOnce(element, 'render-content');
+ assert.isTrue(processStub.calledOnce);
+ assert.isTrue(processStub.lastCall.args[1]);
+ });
+
+ test('binary', async () => {
+ element.diff = {...createEmptyDiff(), content, binary: true};
+ element.render(keyLocations);
+ await waitForEventOnce(element, 'render-content');
+ assert.isTrue(processStub.calledOnce);
+ assert.isTrue(processStub.lastCall.args[1]);
+ });
+ });
+
+ suite('rendering', () => {
+ let content: DiffContent[];
+ let outputEl: HTMLTableElement;
+ let keyLocations: KeyLocations;
+ let addColumnsStub: sinon.SinonStub;
+ let dispatchStub: sinon.SinonStub;
+ let builder: GrDiffBuilderSideBySide;
+
+ setup(() => {
+ 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() as GrDiffBuilderElement;
+ dispatchStub = sinon.stub(element, 'dispatchEvent');
+ outputEl = queryAndAssert(element, '#diffTable');
+ keyLocations = {left: {}, right: {}};
+ sinon.stub(element, '_getDiffBuilder').callsFake(() => {
+ builder = new GrDiffBuilderSideBySide(
+ {...createEmptyDiff(), content},
+ prefs,
+ outputEl
+ );
+ addColumnsStub = 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 = {...createEmptyDiff(), content};
+ element.prefs = prefs;
+ element.render(keyLocations);
+ });
+
+ test('addColumns is called', () => {
+ assert.isTrue(addColumnsStub.called);
+ });
+
+ test('getGroupsByLineRange one line', () => {
+ const section = outputEl.querySelector<HTMLElement>(
+ 'stub:nth-of-type(3)'
+ );
+ const groups = builder.getGroupsByLineRange(1, 1, Side.LEFT);
+ assert.equal(groups.length, 1);
+ assert.strictEqual(groups[0].element, section);
+ });
+
+ test('getGroupsByLineRange over diff', () => {
+ const section = [
+ outputEl.querySelector<HTMLElement>('stub:nth-of-type(3)'),
+ outputEl.querySelector<HTMLElement>('stub:nth-of-type(4)'),
+ ];
+ const groups = builder.getGroupsByLineRange(1, 2, Side.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 () => {
+ await new Promise(resolve => afterNextRender(element, resolve));
+ const firedEventTypes = dispatchStub.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', () => {
+ let dispatchStub: sinon.SinonStub;
+
+ setup(async () => {
+ element = basicFixture.instantiate() as GrDiffBuilderElement;
+ dispatchStub = sinon.stub(element, 'dispatchEvent');
+ const afterNextRenderPromise = new Promise(resolve => {
+ afterNextRender(element, resolve);
+ });
+ element.diff = {
+ ...createEmptyDiff(),
+ 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: KeyLocations = {left: {}, right: {}};
+ element.prefs = {
+ ...DEFAULT_PREFS,
+ context: 1,
+ };
+ 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<HTMLElement>(
+ '.showContext'
+ )[0];
+ assert.isOk(topExpandCommonButton);
+ 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 () => {
+ dispatchStub.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');
+
+ await new Promise(resolve => afterNextRender(element, resolve));
+ const firedEventTypes = dispatchStub.getCalls().map(c => c.args[0].type);
+ assert.include(firedEventTypes, 'render-content');
+ });
+ });
+
+ suite('mock-diff', () => {
+ let element: GrDiffBuilderElement;
+ let builder: GrDiffBuilderSideBySide;
+ let diff: DiffInfo;
+ let keyLocations: KeyLocations;
+
+ setup(() => {
+ element = mockDiffFixture.instantiate() as GrDiffBuilderElement;
+ diff = createDiff();
+ element.diff = diff;
+
+ keyLocations = {left: {}, right: {}};
+
+ element.prefs = {
+ ...createDefaultDiffPrefs(),
+ line_length: 80,
+ show_tabs: true,
+ tab_size: 4,
+ };
+ element.render(keyLocations);
+ builder = element._builder as GrDiffBuilderSideBySide;
+ });
+
+ 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: HTMLElement | null;
+
+ actual = builder.getContentByLine(2, Side.LEFT);
+ assert.equal(actual?.textContent, diff.content[0].ab?.[1]);
+
+ actual = builder.getContentByLine(2, Side.RIGHT);
+ assert.equal(actual?.textContent, diff.content[0].ab?.[1]);
+
+ actual = builder.getContentByLine(5, Side.LEFT);
+ assert.equal(actual?.textContent, diff.content[2].ab?.[0]);
+
+ actual = builder.getContentByLine(5, Side.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 = queryAndAssert(diffRow, 'td.lineNum.left');
+ const lineNumButtonLeft = queryAndAssert(lineNumTdLeft, 'button');
+ const contentTdLeft = diffRow.querySelectorAll('.content')[0];
+
+ const lineNumTdRight = queryAndAssert(diffRow, 'td.lineNum.right');
+ const lineNumButtonRight = queryAndAssert(lineNumTdRight, '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: GrDiffLine[] = [];
+ const elems: HTMLElement[] = [];
+ const start = 6;
+ const end = 10;
+ const count = end - start + 1;
+
+ builder.findLinesByRange(start, end, Side.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, Side.LEFT);
+
+ assert.equal(spy.callCount, count);
+ spy.getCalls().forEach((call, i: number) => {
+ 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((_1, _2, _3, 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, Side.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,
+ Side.LEFT,
+ element.$.diffTable as HTMLTableElement
+ );
+ assert.isOk(contentEl);
+ const lineNumberEl = builder.getLineNumberEl(contentEl!, Side.LEFT);
+ assert.isOk(lineNumberEl);
+ assert.isTrue(lineNumberEl!.classList.contains('lineNum'));
+ assert.isTrue(lineNumberEl!.classList.contains(Side.LEFT));
+ });
+
+ test('getLineNumberEl side-by-side right', () => {
+ const contentEl = builder.getContentByLine(
+ 5,
+ Side.RIGHT,
+ element.$.diffTable as HTMLTableElement
+ );
+ assert.isOk(contentEl);
+ const lineNumberEl = builder.getLineNumberEl(contentEl!, Side.RIGHT);
+ assert.isOk(lineNumberEl);
+ assert.isTrue(lineNumberEl!.classList.contains('lineNum'));
+ assert.isTrue(lineNumberEl!.classList.contains(Side.RIGHT));
+ });
+
+ test('getLineNumberEl unified left', async () => {
+ // Re-render as unified:
+ element.viewMode = 'UNIFIED_DIFF';
+ element.render(keyLocations);
+ builder = element._builder as GrDiffBuilderSideBySide;
+
+ const contentEl = builder.getContentByLine(
+ 5,
+ Side.LEFT,
+ element.$.diffTable as HTMLTableElement
+ );
+ assert.isOk(contentEl);
+ const lineNumberEl = builder.getLineNumberEl(contentEl!, Side.LEFT);
+ assert.isOk(lineNumberEl);
+ assert.isTrue(lineNumberEl!.classList.contains('lineNum'));
+ assert.isTrue(lineNumberEl!.classList.contains(Side.LEFT));
+ });
+
+ test('getLineNumberEl unified right', async () => {
+ // Re-render as unified:
+ element.viewMode = 'UNIFIED_DIFF';
+ element.render(keyLocations);
+ builder = element._builder as GrDiffBuilderSideBySide;
+
+ const contentEl = builder.getContentByLine(
+ 5,
+ Side.RIGHT,
+ element.$.diffTable as HTMLTableElement
+ );
+ assert.isOk(contentEl);
+ const lineNumberEl = builder.getLineNumberEl(contentEl!, Side.RIGHT);
+ assert.isOk(lineNumberEl);
+ assert.isTrue(lineNumberEl!.classList.contains('lineNum'));
+ assert.isTrue(lineNumberEl!.classList.contains(Side.RIGHT));
+ });
+
+ test('getNextContentOnSide side-by-side left', () => {
+ const startElem = builder.getContentByLine(
+ 5,
+ Side.LEFT,
+ element.$.diffTable as HTMLTableElement
+ );
+ assert.isOk(startElem);
+ const expectedStartString = diff.content[2].ab?.[0];
+ const expectedNextString = diff.content[2].ab?.[1];
+ assert.equal(startElem!.textContent, expectedStartString);
+
+ const nextElem = builder.getNextContentOnSide(startElem!, Side.LEFT);
+ assert.isOk(nextElem);
+ assert.equal(nextElem!.textContent, expectedNextString);
+ });
+
+ test('getNextContentOnSide side-by-side right', () => {
+ const startElem = builder.getContentByLine(
+ 5,
+ Side.RIGHT,
+ element.$.diffTable as HTMLTableElement
+ );
+ const expectedStartString = diff.content[1].b?.[0];
+ const expectedNextString = diff.content[1].b?.[1];
+ assert.isOk(startElem);
+ assert.equal(startElem!.textContent, expectedStartString);
+
+ const nextElem = builder.getNextContentOnSide(startElem!, Side.RIGHT);
+ assert.isOk(nextElem);
+ assert.equal(nextElem!.textContent, expectedNextString);
+ });
+
+ test('getNextContentOnSide unified left', async () => {
+ // Re-render as unified:
+ element.viewMode = 'UNIFIED_DIFF';
+ element.render(keyLocations);
+ builder = element._builder as GrDiffBuilderSideBySide;
+
+ const startElem = builder.getContentByLine(
+ 5,
+ Side.LEFT,
+ element.$.diffTable as HTMLTableElement
+ );
+ const expectedStartString = diff.content[2].ab?.[0];
+ const expectedNextString = diff.content[2].ab?.[1];
+ assert.isOk(startElem);
+ assert.equal(startElem!.textContent, expectedStartString);
+
+ const nextElem = builder.getNextContentOnSide(startElem!, Side.LEFT);
+ assert.isOk(nextElem);
+ assert.equal(nextElem!.textContent, expectedNextString);
+ });
+
+ test('getNextContentOnSide unified right', async () => {
+ // Re-render as unified:
+ element.viewMode = 'UNIFIED_DIFF';
+ element.render(keyLocations);
+ builder = element._builder as GrDiffBuilderSideBySide;
+
+ const startElem = builder.getContentByLine(
+ 5,
+ Side.RIGHT,
+ element.$.diffTable as HTMLTableElement
+ );
+ const expectedStartString = diff.content[1].b?.[0];
+ const expectedNextString = diff.content[1].b?.[1];
+ assert.isOk(startElem);
+ assert.equal(startElem!.textContent, expectedStartString);
+
+ const nextElem = builder.getNextContentOnSide(startElem!, Side.RIGHT);
+ assert.isOk(nextElem);
+ assert.equal(nextElem!.textContent, expectedNextString);
+ });
+ });
+
+ suite('blame', () => {
+ let mockBlame: BlameInfo[];
+
+ setup(() => {
+ mockBlame = [
+ {
+ author: 'test-author',
+ time: 314,
+ commit_msg: 'test-commit-message',
+ id: 'commit 1',
+ ranges: [
+ {start: 1, end: 2},
+ {start: 10, end: 16},
+ ],
+ },
+ {
+ author: 'test-author',
+ time: 314,
+ commit_msg: 'test-commit-message',
+ 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(undefined);
+ 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: [{start: 4, end: 10}],
+ };
+ 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>
+ `);
+ });
+ });
+});
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-legacy.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-legacy.ts
index ceadc94..c04d156 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-legacy.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-legacy.ts
@@ -162,10 +162,8 @@
*
* TODO(brohlfs): Consolidate this with getLineEl... methods in html file.
*/
- private getLineNumberEl(
- content: HTMLElement,
- side: Side
- ): HTMLElement | null {
+ // visible for testing
+ getLineNumberEl(content: HTMLElement, side: Side): HTMLElement | null {
let row: HTMLElement | null = content;
while (row && !row.classList.contains('diff-row')) row = row.parentElement;
return row ? (row.querySelector('.lineNum.' + side) as HTMLElement) : null;
@@ -349,7 +347,8 @@
});
}
- protected createTextEl(
+ // visible for testing
+ createTextEl(
lineNumberEl: HTMLElement | null,
line: GrDiffLine,
side?: Side
@@ -491,7 +490,8 @@
* Create a blame cell for the given base line. Blame information will be
* included in the cell if available.
*/
- protected createBlameCell(lineNumber: LineNumber): HTMLTableCellElement {
+ // visible for testing
+ createBlameCell(lineNumber: LineNumber): HTMLTableCellElement {
const blameTd = createElementDiff('td', 'blame') as HTMLTableCellElement;
blameTd.setAttribute('data-line-number', lineNumber.toString());
if (!lineNumber) return blameTd;
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-side-by-side.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-side-by-side.ts
index a711215..f2690bc 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-side-by-side.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-side-by-side.ts
@@ -44,7 +44,8 @@
};
}
- protected override buildSectionElement(group: GrDiffGroup) {
+ // visible for testing
+ override buildSectionElement(group: GrDiffGroup) {
const sectionEl = createElementDiff('tbody', 'section');
sectionEl.classList.add(group.type);
if (group.isTotal()) {
@@ -147,7 +148,8 @@
return td;
}
- protected override getNextContentOnSide(
+ // visible for testing
+ override getNextContentOnSide(
content: HTMLElement,
side: Side
): HTMLElement | null {
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder.ts
index add7ffa..4b664e2 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder.ts
@@ -89,7 +89,8 @@
protected readonly numLinesLeft: number;
- protected readonly _prefs: DiffPreferencesInfo;
+ // visible for testing
+ readonly _prefs: DiffPreferencesInfo;
protected readonly renderPrefs?: RenderPreferences;
@@ -194,7 +195,8 @@
group.element = element;
}
- private getGroupsByLineRange(
+ // visible for testing
+ getGroupsByLineRange(
startLine: LineNumber,
endLine: LineNumber,
side: Side
@@ -257,7 +259,8 @@
* TODO: Change `null` to `undefined` in paramete type. Also: Do we
* really need to support null/undefined? Also change to camelCase.
*/
- protected findLinesByRange(
+ // visible for testing
+ findLinesByRange(
start: LineNumber,
end: LineNumber,
side: Side,
@@ -352,9 +355,8 @@
*
* @return The commit information.
*/
- protected getBlameCommitForBaseLine(
- lineNum: LineNumber
- ): BlameInfo | undefined {
+ // visible for testing
+ getBlameCommitForBaseLine(lineNum: LineNumber): BlameInfo | undefined {
for (const blameCommit of this.blameInfo) {
for (const range of blameCommit.ranges) {
if (range.start <= lineNum && range.end >= lineNum) {
diff --git a/polygerrit-ui/app/test/test-data-generators.ts b/polygerrit-ui/app/test/test-data-generators.ts
index 829a36b..8e8fe42 100644
--- a/polygerrit-ui/app/test/test-data-generators.ts
+++ b/polygerrit-ui/app/test/test-data-generators.ts
@@ -469,6 +469,24 @@
};
}
+export function createEmptyDiff(): DiffInfo {
+ return {
+ meta_a: {
+ name: 'empty-left.txt',
+ content_type: 'text/plain',
+ lines: 1,
+ },
+ meta_b: {
+ name: 'empty-right.txt',
+ content_type: 'text/plain',
+ lines: 1,
+ },
+ intraline_status: 'OK',
+ change_type: 'MODIFIED',
+ content: [],
+ };
+}
+
export function createDiff(): DiffInfo {
return {
meta_a: {