Merge changes I83f0153b,I9e4aa92a,I019ce1c6,I7b0bc949,I19d4515d, ...
* changes:
Migrate gr-diff-builder-unified_test from js to ts
Migrate gr-diff-cursor_test from js to ts
Migrate gr-diff-app-context-init_test from js to ts
Migrate gr-diff-group_test from js to ts
Migrate gr-diff_test from js to ts
Migrate gr-diff-builder-element from Polymer to plain class
Migrate gr-diff-builder-element_test from js to ts
diff --git a/polygerrit-ui/app/api/diff.ts b/polygerrit-ui/app/api/diff.ts
index 6788aa3..4322c64 100644
--- a/polygerrit-ui/app/api/diff.ts
+++ b/polygerrit-ui/app/api/diff.ts
@@ -38,10 +38,16 @@
* If the weblinks-only parameter is specified, only the web_links field is set.
*/
export declare interface DiffInfo {
- /** Meta information about the file on side A as a DiffFileMetaInfo entity. */
- meta_a: DiffFileMetaInfo;
- /** Meta information about the file on side B as a DiffFileMetaInfo entity. */
- meta_b: DiffFileMetaInfo;
+ /**
+ * Meta information about the file on side A as a DiffFileMetaInfo entity.
+ * Not set when change_type is ADDED.
+ */
+ meta_a?: DiffFileMetaInfo;
+ /**
+ * Meta information about the file on side B as a DiffFileMetaInfo entity.
+ * Not set when change_type is DELETED.
+ */
+ meta_b?: DiffFileMetaInfo;
/** The type of change (ADDED, MODIFIED, DELETED, RENAMED COPIED, REWRITE). */
change_type: ChangeType;
/** Intraline status (OK, ERROR, TIMEOUT). */
@@ -167,7 +173,7 @@
* Indicates the range (line numbers) on the other side of the comparison
* where the code related to the current chunk came from/went to.
*/
- range: {
+ range?: {
start: number;
end: number;
};
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
index dda8490..88aae5c 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
@@ -313,7 +313,7 @@
// Recognizes that it should be an image diff.
assert.isTrue(element.isImageDiff);
assert.instanceOf(
- element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage);
+ element.$.diff.diffBuilder.builder, GrDiffBuilderImage);
// Left image rendered with the parent commit's version of the file.
const leftImage =
@@ -381,7 +381,7 @@
// Recognizes that it should be an image diff.
assert.isTrue(element.isImageDiff);
assert.instanceOf(
- element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage);
+ element.$.diff.diffBuilder.builder, GrDiffBuilderImage);
// Left image rendered with the parent commit's version of the file.
const leftImage =
@@ -445,7 +445,7 @@
// Recognizes that it should be an image diff.
assert.isTrue(element.isImageDiff);
assert.instanceOf(
- element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage);
+ element.$.diff.diffBuilder.builder, GrDiffBuilderImage);
const leftImage =
element.$.diff.$.diffTable.querySelector('td.left img');
@@ -493,7 +493,7 @@
// Recognizes that it should be an image diff.
assert.isTrue(element.isImageDiff);
assert.instanceOf(
- element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage);
+ element.$.diff.diffBuilder.builder, GrDiffBuilderImage);
const leftImage =
element.$.diff.$.diffTable.querySelector('td.left img');
@@ -543,7 +543,7 @@
// Recognizes that it should be an image diff.
assert.isTrue(element.isImageDiff);
assert.instanceOf(
- element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage);
+ element.$.diff.diffBuilder.builder, GrDiffBuilderImage);
const leftImage =
element.$.diff.$.diffTable.querySelector('td.left img');
assert.isNotOk(leftImage);
@@ -623,7 +623,7 @@
test('clearBlame', () => {
element._blame = [];
- const setBlameSpy = sinon.spy(element.$.diff.$.diffBuilder, 'setBlame');
+ const setBlameSpy = sinon.spy(element.$.diff.diffBuilder, 'setBlame');
element.clearBlame();
assert.isNull(element._blame);
assert.isTrue(setBlameSpy.calledWithExactly(null));
diff --git a/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls.ts b/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls.ts
index a5effaf..47f5f81 100644
--- a/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls.ts
+++ b/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls.ts
@@ -462,7 +462,7 @@
numLines: number,
referenceLine: number
) {
- assertIsDefined(this.diff, 'diff');
+ if (!this.diff?.meta_b) return;
const syntaxTree = this.diff.meta_b.syntax_tree;
const outlineSyntaxPath = findBlockTreePathForLine(
referenceLine,
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..d200c75 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
@@ -1,24 +1,11 @@
/**
* @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.
+ * Copyright 2016 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
*/
import '../gr-diff-processor/gr-diff-processor';
import '../../../elements/shared/gr-hovercard/gr-hovercard';
import './gr-diff-builder-side-by-side';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-diff-builder-element_html';
import {GrAnnotation} from '../gr-diff-highlight/gr-annotation';
import {DiffBuilder, DiffContextExpandedEventDetail} from './gr-diff-builder';
import {GrDiffBuilderSideBySide} from './gr-diff-builder-side-by-side';
@@ -26,7 +13,6 @@
import {GrDiffBuilderUnified} from './gr-diff-builder-unified';
import {GrDiffBuilderBinary} from './gr-diff-builder-binary';
import {CancelablePromise, makeCancelable} from '../../../scripts/util';
-import {customElement, property, observe} from '@polymer/decorators';
import {BlameInfo, ImageInfo} from '../../../types/common';
import {DiffInfo, DiffPreferencesInfo} from '../../../types/diff';
import {CoverageRange, DiffLayer} from '../../../types/types';
@@ -53,18 +39,35 @@
hideInContextControl,
} from '../gr-diff/gr-diff-group';
import {getLineNumber, getSideByLineEl} from '../gr-diff/gr-diff-utils';
-import {fireAlert, fireEvent, fire} from '../../../utils/event-util';
-import {afterNextRender} from '@polymer/polymer/lib/utils/render-status';
+import {
+ fireAlert,
+ fire,
+ HTMLElementEventDetailType,
+} from '../../../utils/event-util';
+import {assertIsDefined} from '../../../utils/common-util';
+import {afterNextRender} from '../../../utils/dom-util';
const TRAILING_WHITESPACE_PATTERN = /\s+$/;
-
-// https://gerrit.googlesource.com/gerrit/+/234616a8627334686769f1de989d286039f4d6a5/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js#740
const COMMIT_MSG_PATH = '/COMMIT_MSG';
const COMMIT_MSG_LINE_LENGTH = 72;
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<{}>;
}
}
@@ -97,112 +100,59 @@
}
}
-@customElement('gr-diff-builder')
-export class GrDiffBuilderElement
- extends PolymerElement
- implements GroupConsumer
-{
- static get template() {
- 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})
+// TODO: Rename the class and the file and remove "element". This is not an
+// element anymore.
+export class GrDiffBuilderElement implements GroupConsumer {
diff?: DiffInfo;
- @property({type: String})
+ diffElement?: HTMLTableElement;
+
viewMode?: string;
- @property({type: Boolean})
isImageDiff?: boolean;
- @property({type: Object})
baseImage: ImageInfo | null = null;
- @property({type: Object})
revisionImage: ImageInfo | null = null;
- @property({type: Number})
- parentIndex?: number;
-
- @property({type: String})
path?: string;
- @property({type: Object})
prefs: DiffPreferencesInfo = createDefaultDiffPrefs();
- @property({type: Object})
renderPrefs?: RenderPreferences;
- @property({type: Object})
- _builder?: DiffBuilder;
-
- /**
- * The gr-diff-processor adds (and only adds!) to this array. It does so by
- * using `this.push()` and Polymer's two-way data binding.
- * Below (@observe('_groups.splices')) we are observing the groups that the
- * processor adds, and pass them on to the builder for rendering. Henceforth
- * the builder groups are the source of truth, because when
- * expanding/collapsing groups only the builder is updated. This field and the
- * corresponsing one in the processor are not updated.
- */
- @property({type: Array})
- _groups: GrDiffGroup[] = [];
+ useNewImageDiffUi = false;
/**
* Layers passed in from the outside.
+ *
+ * See `layersInternal` for where these layers will end up together with the
+ * internal layers.
*/
- @property({type: Array})
layers: DiffLayer[] = [];
+ // visible for testing
+ builder?: DiffBuilder;
+
/**
- * All layers, both from the outside and the default ones.
+ * All layers, both from the outside and the default ones. See `layers` for
+ * the property that can be set from the outside.
*/
- @property({type: Array})
- _layers: DiffLayer[] = [];
+ // visible for testing
+ layersInternal: DiffLayer[] = [];
- @property({type: Boolean})
- _showTabs?: boolean;
+ // visible for testing
+ showTabs?: boolean;
- @property({type: Boolean})
- _showTrailingWhitespace?: boolean;
-
- @property({type: Array})
- commentRanges: CommentRangeLayer[] = [];
-
- @property({type: Array, observer: 'coverageObserver'})
- coverageRanges: CoverageRange[] = [];
-
- @property({type: Boolean})
- useNewImageDiffUi = false;
+ // visible for testing
+ showTrailingWhitespace?: boolean;
/**
* The promise last returned from `render()` while the asynchronous
* rendering is running - `null` otherwise. Provides a `cancel()`
* method that rejects it with `{isCancelled: true}`.
*/
- @property({type: Object})
- _cancelableRenderPromise: CancelablePromise<unknown> | null = null;
+ private cancelableRenderPromise: CancelablePromise<unknown> | null = null;
private coverageLayerLeft = new GrCoverageLayer(Side.LEFT);
@@ -210,51 +160,20 @@
private rangeLayer = new GrRangedCommentLayer();
- private processor = new GrDiffProcessor();
+ // visible for testing
+ processor = new GrDiffProcessor();
constructor() {
- super();
- afterNextRender(this, () => {
- this.addEventListener(
- 'diff-context-expanded',
- (e: CustomEvent<DiffContextExpandedEventDetail>) => {
- // Don't stop propagation. The host may listen for reporting or
- // resizing.
- this.replaceGroup(e.detail.contextGroup, e.detail.groups);
- }
- );
- });
this.processor.consumer = this;
}
- override disconnectedCallback() {
- this.processor.cancel();
- if (this._builder) {
- this._builder.clear();
- }
- super.disconnectedCallback();
+ updateCommentRanges(ranges: CommentRangeLayer[]) {
+ this.rangeLayer.updateRanges(ranges);
}
- get diffElement(): HTMLTableElement {
- // Not searching in shadowRoot, because the diff table is slotted!
- return this.querySelector('#diffTable') as HTMLTableElement;
- }
-
- @observe('commentRanges.*')
- rangeObserver() {
- this.rangeLayer.updateRanges(this.commentRanges);
- }
-
- coverageObserver(coverageRanges: CoverageRange[]) {
- const leftRanges = coverageRanges.filter(
- range => range && range.side === Side.LEFT
- );
- this.coverageLayerLeft.setRanges(leftRanges);
-
- const rightRanges = coverageRanges.filter(
- range => range && range.side === Side.RIGHT
- );
- this.coverageLayerRight.setRanges(rightRanges);
+ updateCoverageRanges(rs: CoverageRange[]) {
+ this.coverageLayerLeft.setRanges(rs.filter(r => r?.side === Side.LEFT));
+ this.coverageLayerRight.setRanges(rs.filter(r => r?.side === Side.RIGHT));
}
render(keyLocations: KeyLocations): void {
@@ -262,42 +181,44 @@
// installed, and |render| satisfies the requirement, however,
// |attached| doesn't because in the diff view page, the element is
// attached before plugins are installed.
- this._setupAnnotationLayers();
+ this.setupAnnotationLayers();
- this._showTabs = this.prefs.show_tabs;
- this._showTrailingWhitespace = this.prefs.show_whitespace_errors;
+ this.showTabs = this.prefs.show_tabs;
+ this.showTrailingWhitespace = this.prefs.show_whitespace_errors;
// Stop the processor if it's running.
this.cancel();
- if (this._builder) {
- this._builder.clear();
- }
- if (!this.diff) {
- throw Error('Cannot render a diff without DiffInfo.');
- }
- this._builder = this._getDiffBuilder();
+ this.builder?.clear();
+ assertIsDefined(this.diff, 'diff');
+ assertIsDefined(this.diffElement, 'diff table');
+ this.builder = this.getDiffBuilder();
this.processor.context = this.prefs.context;
this.processor.keyLocations = keyLocations;
- this._clearDiffContent();
- this._builder.addColumns(
+ this.diffElement.addEventListener(
+ 'diff-context-expanded',
+ this.onDiffContextExpanded
+ );
+
+ this.clearDiffContent();
+ this.builder.addColumns(
this.diffElement,
getLineNumberCellWidth(this.prefs)
);
const isBinary = !!(this.isImageDiff || this.diff.binary);
- fireEvent(this, 'render-start');
- this._cancelableRenderPromise = makeCancelable(
+ this.fireDiffEvent('render-start', {});
+ this.cancelableRenderPromise = makeCancelable(
this.processor
.process(this.diff.content, isBinary)
.then(() => {
if (this.isImageDiff) {
- (this._builder as GrDiffBuilderImage).renderDiff();
+ (this.builder as GrDiffBuilderImage).renderDiff();
}
- afterNextRender(this, () => fireEvent(this, 'render-content'));
+ afterNextRender(() => this.fireDiffEvent('render-content', {}));
})
// Mocha testing does not like uncaught rejections, so we catch
// the cancels which are expected and should not throw errors in
@@ -307,17 +228,39 @@
return;
})
.finally(() => {
- this._cancelableRenderPromise = null;
+ this.cancelableRenderPromise = null;
})
);
}
- _setupAnnotationLayers() {
+ private onDiffContextExpanded = (
+ e: CustomEvent<DiffContextExpandedEventDetail>
+ ) => {
+ // Don't stop propagation. The host may listen for reporting or
+ // resizing.
+ this.replaceGroup(e.detail.contextGroup, e.detail.groups);
+ };
+
+ private fireDiffEvent<K extends keyof HTMLElementEventMap>(
+ type: K,
+ detail: HTMLElementEventDetailType<K>
+ ) {
+ assertIsDefined(this.diffElement, 'diff table');
+ fire(this.diffElement, type, detail);
+ }
+
+ private fireDiffEventRenderProgress(detail: RenderProgressEventDetail) {
+ assertIsDefined(this.diffElement, 'diff table');
+ fire(this.diffElement, 'render-progress', detail);
+ }
+
+ // visible for testing
+ setupAnnotationLayers() {
const layers: DiffLayer[] = [
- this._createTrailingWhitespaceLayer(),
- this._createIntralineLayer(),
- this._createTabIndicatorLayer(),
- this._createSpecialCharacterIndicatorLayer(),
+ this.createTrailingWhitespaceLayer(),
+ this.createIntralineLayer(),
+ this.createTabIndicatorLayer(),
+ this.createSpecialCharacterIndicatorLayer(),
this.rangeLayer,
this.coverageLayerLeft,
this.coverageLayerRight,
@@ -326,15 +269,15 @@
if (this.layers) {
layers.push(...this.layers);
}
- this._layers = layers;
+ this.layersInternal = layers;
}
getContentTdByLine(lineNumber: LineNumber, side?: Side, root?: Element) {
- if (!this._builder) return null;
- return this._builder.getContentTdByLine(lineNumber, side, root);
+ if (!this.builder) return null;
+ return this.builder.getContentTdByLine(lineNumber, side, root);
}
- _getDiffRowByChild(child: Element) {
+ private getDiffRowByChild(child: Element) {
while (!child.classList.contains('diff-row') && child.parentElement) {
child = child.parentElement;
}
@@ -348,23 +291,23 @@
const side = getSideByLineEl(lineEl);
// Performance optimization because we already have an element in the
// correct row
- const row = this._getDiffRowByChild(lineEl);
+ const row = this.getDiffRowByChild(lineEl);
return this.getContentTdByLine(line, side, row);
}
getLineElByNumber(lineNumber: LineNumber, side?: Side) {
- if (!this._builder) return null;
- return this._builder.getLineElByNumber(lineNumber, side);
+ if (!this.builder) return null;
+ return this.builder.getLineElByNumber(lineNumber, side);
}
getLineNumberRows() {
- if (!this._builder) return [];
- return this._builder.getLineNumberRows();
+ if (!this.builder) return [];
+ return this.builder.getLineNumberRows();
}
getLineNumEls(side: Side) {
- if (!this._builder) return [];
- return this._builder.getLineNumEls(side);
+ if (!this.builder) return [];
+ return this.builder.getLineNumEls(side);
}
/**
@@ -376,8 +319,8 @@
* @param side The side the line number refer to.
*/
unhideLine(lineNum: number, side: Side) {
- if (!this._builder) return;
- const group = this._builder.findGroup(side, lineNum);
+ if (!this.builder) return;
+ const group = this.builder.findGroup(side, lineNum);
// Cannot unhide a line that is not part of the diff.
if (!group) return;
// If it's already visible, great!
@@ -420,45 +363,50 @@
contextGroup: GrDiffGroup,
newGroups: readonly GrDiffGroup[]
) {
- if (!this._builder) return;
- fireEvent(this, 'render-start');
+ if (!this.builder) return;
+ this.fireDiffEvent('render-start', {});
const linesRendered = newGroups.reduce(
(sum, group) => sum + group.lines.length,
0
);
- this._builder.replaceGroup(contextGroup, newGroups);
- afterNextRender(this, () => {
- fire(this, 'render-progress', {linesRendered});
- fireEvent(this, 'render-content');
+ this.builder.replaceGroup(contextGroup, newGroups);
+ afterNextRender(() => {
+ this.fireDiffEvent('render-progress', {linesRendered});
+ this.fireDiffEvent('render-content', {});
});
}
cancel() {
this.processor.cancel();
- if (this._cancelableRenderPromise) {
- this._cancelableRenderPromise.cancel();
- this._cancelableRenderPromise = null;
- }
+ this.builder?.clear();
+ this.cancelableRenderPromise?.cancel();
+ this.cancelableRenderPromise = null;
+ this.diffElement?.removeEventListener(
+ 'diff-context-expanded',
+ this.onDiffContextExpanded
+ );
}
- _handlePreferenceError(pref: string): never {
+ // visible for testing
+ handlePreferenceError(pref: string): never {
const message =
`The value of the '${pref}' user preference is ` +
'invalid. Fix in diff preferences';
- fireAlert(this, message);
+ assertIsDefined(this.diffElement, 'diff table');
+ fireAlert(this.diffElement, message);
throw Error(`Invalid preference value: ${pref}`);
}
- _getDiffBuilder(): DiffBuilder {
- if (!this.diff) {
- throw Error('Cannot render a diff without DiffInfo.');
- }
+ // visible for testing
+ getDiffBuilder(): DiffBuilder {
+ assertIsDefined(this.diff, 'diff');
+ assertIsDefined(this.diffElement, 'diff table');
if (isNaN(this.prefs.tab_size) || this.prefs.tab_size <= 0) {
- this._handlePreferenceError('tab size');
+ this.handlePreferenceError('tab size');
}
if (isNaN(this.prefs.line_length) || this.prefs.line_length <= 0) {
- this._handlePreferenceError('diff width');
+ this.handlePreferenceError('diff width');
}
const localPrefs = {...this.prefs};
@@ -487,7 +435,7 @@
this.diff,
localPrefs,
this.diffElement,
- this._layers,
+ this.layersInternal,
this.renderPrefs
);
} else if (this.viewMode === DiffViewMode.UNIFIED) {
@@ -495,7 +443,7 @@
this.diff,
localPrefs,
this.diffElement,
- this._layers,
+ this.layersInternal,
this.renderPrefs
);
}
@@ -505,7 +453,8 @@
return builder;
}
- _clearDiffContent() {
+ private clearDiffContent() {
+ assertIsDefined(this.diffElement, 'diff table');
this.diffElement.innerHTML = '';
}
@@ -514,22 +463,23 @@
* server into chunks.
*/
clearGroups() {
- if (!this._builder) return;
- this._builder.clearGroups();
+ if (!this.builder) return;
+ this.builder.clearGroups();
}
/**
* Called when the processor is done converting a chunk of the diff.
*/
addGroup(group: GrDiffGroup) {
- if (!this._builder) return;
- this._builder.addGroups([group]);
- afterNextRender(this, () =>
- fire(this, 'render-progress', {linesRendered: group.lines.length})
+ if (!this.builder) return;
+ this.builder.addGroups([group]);
+ afterNextRender(() =>
+ this.fireDiffEventRenderProgress({linesRendered: group.lines.length})
);
}
- _createIntralineLayer(): DiffLayer {
+ // visible for testing
+ createIntralineLayer(): DiffLayer {
return {
// Take a DIV.contentText element and a line object with intraline
// differences to highlight and apply them to the element as
@@ -561,8 +511,9 @@
};
}
- _createTabIndicatorLayer(): DiffLayer {
- const show = () => this._showTabs;
+ // visible for testing
+ createTabIndicatorLayer(): DiffLayer {
+ const show = () => this.showTabs;
return {
annotate(contentEl: HTMLElement, _: HTMLElement, line: GrDiffLine) {
// If visible tabs are disabled, do nothing.
@@ -576,7 +527,7 @@
};
}
- _createSpecialCharacterIndicatorLayer(): DiffLayer {
+ private createSpecialCharacterIndicatorLayer(): DiffLayer {
return {
annotate(contentEl: HTMLElement, _: HTMLElement, line: GrDiffLine) {
// Find and annotate the locations of soft hyphen (\u00AD)
@@ -592,8 +543,9 @@
};
}
- _createTrailingWhitespaceLayer(): DiffLayer {
- const show = () => this._showTrailingWhitespace;
+ // visible for testing
+ createTrailingWhitespaceLayer(): DiffLayer {
+ const show = () => this.showTrailingWhitespace;
return {
annotate(contentEl: HTMLElement, _: HTMLElement, line: GrDiffLine) {
@@ -621,18 +573,12 @@
}
setBlame(blame: BlameInfo[] | null) {
- if (!this._builder) return;
- this._builder.setBlame(blame ?? []);
+ if (!this.builder) return;
+ this.builder.setBlame(blame ?? []);
}
updateRenderPrefs(renderPrefs: RenderPreferences) {
- this._builder?.updateRenderPrefs(renderPrefs);
+ this.builder?.updateRenderPrefs(renderPrefs);
this.processor.updateRenderPrefs(renderPrefs);
}
}
-
-declare global {
- interface HTMLElementTagNameMap {
- 'gr-diff-builder': GrDiffBuilderElement;
- }
-}
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element_html.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element_html.ts
deleted file mode 100644
index bd0e034..0000000
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element_html.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 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 {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <div class="contentWrapper">
- <slot></slot>
- </div>
-`;
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..8ae08f4
--- /dev/null
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element_test.ts
@@ -0,0 +1,1131 @@
+/**
+ * @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 {
+ nextRender,
+ 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 {
+ DiffContent,
+ DiffInfo,
+ DiffLayer,
+ DiffPreferencesInfo,
+ DiffViewMode,
+ Side,
+} from '../../../api/diff';
+import {stubRestApi} from '../../../test/test-utils';
+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';
+import {fixture, html} from '@open-wc/testing-helpers';
+
+const DEFAULT_PREFS = createDefaultDiffPrefs();
+
+suite('gr-diff-builder tests', () => {
+ let element: GrDiffBuilderElement;
+ let builder: GrDiffBuilderLegacy;
+ let diffTable: HTMLTableElement;
+
+ 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},
+ diffTable
+ );
+ };
+
+ const line = (text: string) => {
+ const line = new GrDiffLine(GrDiffLineType.BOTH);
+ line.text = text;
+ return line;
+ };
+
+ setup(async () => {
+ diffTable = await fixture(html`<table id="diffTable"></table>`);
+ element = new GrDiffBuilderElement();
+ element.diffElement = diffTable;
+ 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();
+ diffTable.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(async () => {
+ el = await fixture(html`
+ <div>Lorem ipsum dolor sit amet, suspendisse inceptos vehicula</div>
+ `);
+ str = el.textContent ?? '';
+ annotateElementSpy = sinon.spy(GrAnnotation, 'annotateElement');
+ layer = element.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 layer: DiffLayer;
+ const lineNumberEl = document.createElement('td');
+
+ setup(() => {
+ 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 initialLayersCount = 0;
+ let withLayerCount = 0;
+ setup(() => {
+ const layers: DiffLayer[] = [];
+ element.layers = layers;
+ element.showTrailingWhitespace = true;
+ element.setupAnnotationLayers();
+ initialLayersCount = element.layersInternal.length;
+ });
+
+ test('no layers', () => {
+ element.setupAnnotationLayers();
+ assert.equal(element.layersInternal.length, initialLayersCount);
+ });
+
+ suite('with layers', () => {
+ const layers: DiffLayer[] = [{annotate: () => {}}, {annotate: () => {}}];
+ setup(() => {
+ element.layers = layers;
+ element.showTrailingWhitespace = true;
+ element.setupAnnotationLayers();
+ withLayerCount = element.layersInternal.length;
+ });
+ test('with layers', () => {
+ element.setupAnnotationLayers();
+ assert.equal(element.layersInternal.length, withLayerCount);
+ assert.equal(initialLayersCount + layers.length, withLayerCount);
+ });
+ });
+ });
+
+ suite('trailing whitespace', () => {
+ let layer: DiffLayer;
+ const lineNumberEl = document.createElement('td');
+
+ setup(() => {
+ 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.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(diffTable, '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(diffTable, '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(diffTable, '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',
+ ],
+ },
+ ];
+ dispatchStub = sinon.stub(diffTable, 'dispatchEvent');
+ outputEl = element.diffElement!;
+ 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 nextRender();
+ let firedEventTypes = dispatchStub.getCalls().map(c => c.args[0].type);
+ assert.include(firedEventTypes, 'render-start');
+ assert.include(firedEventTypes, 'render-progress');
+
+ await nextRender();
+ firedEventTypes = dispatchStub.getCalls().map(c => c.args[0].type);
+ 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 () => {
+ dispatchStub = sinon.stub(diffTable, 'dispatchEvent');
+ 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 nextRender();
+ });
+
+ test('hides lines behind two context controls', () => {
+ const contextControls = diffTable.querySelectorAll('gr-context-controls');
+ assert.equal(contextControls.length, 2);
+
+ const diffRows = diffTable.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 = diffTable.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 = diffTable.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 = diffTable.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 nextRender();
+ const firedEventTypes = dispatchStub.getCalls().map(c => c.args[0].type);
+ assert.include(firedEventTypes, 'render-content');
+ });
+ });
+
+ suite('mock-diff', () => {
+ let builder: GrDiffBuilderSideBySide;
+ let diff: DiffInfo;
+ let keyLocations: KeyLocations;
+
+ setup(() => {
+ element.viewMode = DiffViewMode.SIDE_BY_SIDE;
+ 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 = diffTable.querySelectorAll(
+ '.lineNumButton.right'
+ )[5];
+
+ assert.isOk(deltaLineNumberButton);
+ assert.equal(deltaLineNumberButton.getAttribute('aria-label'), '5 added');
+ });
+
+ test('aria-labels on removed line numbers', () => {
+ const deltaLineNumberButton = diffTable.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 = diffTable.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.diffElement 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.diffElement 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.diffElement 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.diffElement 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.diffElement 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.diffElement 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.diffElement 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.diffElement 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-unified.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-unified.ts
index 4145485..0c9d1d9 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-unified.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-unified.ts
@@ -43,7 +43,8 @@
};
}
- protected override buildSectionElement(group: GrDiffGroup): HTMLElement {
+ // visible for testing
+ override buildSectionElement(group: GrDiffGroup): HTMLElement {
const sectionEl = createElementDiff('tbody', 'section');
sectionEl.classList.add(group.type);
if (group.isTotal()) {
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-unified_test.js b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-unified_test.js
deleted file mode 100644
index 5f3fb72..0000000
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-unified_test.js
+++ /dev/null
@@ -1,242 +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 '../gr-diff/gr-diff-group.js';
-import './gr-diff-builder.js';
-import './gr-diff-builder-unified.js';
-import {GrDiffLine, GrDiffLineType} from '../gr-diff/gr-diff-line.js';
-import {GrDiffGroup, GrDiffGroupType} from '../gr-diff/gr-diff-group.js';
-import {GrDiffBuilderUnified} from './gr-diff-builder-unified.js';
-
-suite('GrDiffBuilderUnified tests', () => {
- let prefs;
- let outputEl;
- let diffBuilder;
-
- setup(()=> {
- prefs = {
- line_length: 10,
- show_tabs: true,
- tab_size: 4,
- };
- outputEl = document.createElement('div');
- diffBuilder = new GrDiffBuilderUnified({}, prefs, outputEl, []);
- });
-
- suite('buildSectionElement for BOTH group', () => {
- let lines;
- let group;
-
- setup(() => {
- lines = [
- new GrDiffLine(GrDiffLineType.BOTH, 1, 2),
- new GrDiffLine(GrDiffLineType.BOTH, 2, 3),
- new GrDiffLine(GrDiffLineType.BOTH, 3, 4),
- ];
- lines[0].text = 'def hello_world():';
- lines[1].text = ' print "Hello World";';
- lines[2].text = ' return True';
-
- group = new GrDiffGroup({type: GrDiffGroupType.BOTH, lines});
- });
-
- test('creates the section', () => {
- const sectionEl = diffBuilder.buildSectionElement(group);
- assert.isTrue(sectionEl.classList.contains('section'));
- assert.isTrue(sectionEl.classList.contains('both'));
- });
-
- test('creates each unchanged row once', () => {
- const sectionEl = diffBuilder.buildSectionElement(group);
- const rowEls = sectionEl.querySelectorAll('.diff-row');
-
- assert.equal(rowEls.length, 3);
-
- assert.equal(
- rowEls[0].querySelector('.lineNum.left').textContent,
- lines[0].beforeNumber);
- assert.equal(
- rowEls[0].querySelector('.lineNum.right').textContent,
- lines[0].afterNumber);
- assert.equal(
- rowEls[0].querySelector('.content').textContent, lines[0].text);
-
- assert.equal(
- rowEls[1].querySelector('.lineNum.left').textContent,
- lines[1].beforeNumber);
- assert.equal(
- rowEls[1].querySelector('.lineNum.right').textContent,
- lines[1].afterNumber);
- assert.equal(
- rowEls[1].querySelector('.content').textContent, lines[1].text);
-
- assert.equal(
- rowEls[2].querySelector('.lineNum.left').textContent,
- lines[2].beforeNumber);
- assert.equal(
- rowEls[2].querySelector('.lineNum.right').textContent,
- lines[2].afterNumber);
- assert.equal(
- rowEls[2].querySelector('.content').textContent, lines[2].text);
- });
- });
-
- suite('buildSectionElement for moved chunks', () => {
- test('creates a moved out group', () => {
- const lines = [
- new GrDiffLine(GrDiffLineType.REMOVE, 15),
- new GrDiffLine(GrDiffLineType.REMOVE, 16),
- ];
- lines[0].text = 'def hello_world():';
- lines[1].text = ' print "Hello World"';
- const group = new GrDiffGroup({type: GrDiffGroupType.DELTA, lines});
- group.moveDetails = {changed: false};
-
- const sectionEl = diffBuilder.buildSectionElement(group);
-
- const rowEls = sectionEl.querySelectorAll('tr');
- const moveControlsRow = rowEls[0];
- const cells = moveControlsRow.querySelectorAll('td');
- assert.isTrue(sectionEl.classList.contains('dueToMove'));
- assert.equal(rowEls.length, 3);
- assert.isTrue(moveControlsRow.classList.contains('movedOut'));
- assert.equal(cells.length, 3);
- assert.isTrue(cells[2].classList.contains('moveHeader'));
- assert.equal(cells[2].textContent, 'Moved out');
- });
-
- test('creates a moved in group', () => {
- const lines = [
- new GrDiffLine(GrDiffLineType.ADD, 37),
- new GrDiffLine(GrDiffLineType.ADD, 38),
- ];
- lines[0].text = 'def hello_world():';
- lines[1].text = ' print "Hello World"';
- const group = new GrDiffGroup({type: GrDiffGroupType.DELTA, lines});
- group.moveDetails = {changed: false};
-
- const sectionEl = diffBuilder.buildSectionElement(group);
-
- const rowEls = sectionEl.querySelectorAll('tr');
- const moveControlsRow = rowEls[0];
- const cells = moveControlsRow.querySelectorAll('td');
- assert.isTrue(sectionEl.classList.contains('dueToMove'));
- assert.equal(rowEls.length, 3);
- assert.isTrue(moveControlsRow.classList.contains('movedIn'));
- assert.equal(cells.length, 3);
- assert.isTrue(cells[2].classList.contains('moveHeader'));
- assert.equal(cells[2].textContent, 'Moved in');
- });
- });
-
- suite('buildSectionElement for DELTA group', () => {
- let lines;
- let group;
-
- setup(() => {
- lines = [
- new GrDiffLine(GrDiffLineType.REMOVE, 1),
- new GrDiffLine(GrDiffLineType.REMOVE, 2),
- new GrDiffLine(GrDiffLineType.ADD, 2),
- new GrDiffLine(GrDiffLineType.ADD, 3),
- ];
- lines[0].text = 'def hello_world():';
- lines[1].text = ' print "Hello World"';
- lines[2].text = 'def hello_universe()';
- lines[3].text = ' print "Hello Universe"';
-
- group = new GrDiffGroup({type: GrDiffGroupType.DELTA, lines});
- });
-
- test('creates the section', () => {
- const sectionEl = diffBuilder.buildSectionElement(group);
- assert.isTrue(sectionEl.classList.contains('section'));
- assert.isTrue(sectionEl.classList.contains('delta'));
- });
-
- test('creates the section with class if ignoredWhitespaceOnly', () => {
- group.ignoredWhitespaceOnly = true;
- const sectionEl = diffBuilder.buildSectionElement(group);
- assert.isTrue(sectionEl.classList.contains('ignoredWhitespaceOnly'));
- });
-
- test('creates the section with class if dueToRebase', () => {
- group.dueToRebase = true;
- const sectionEl = diffBuilder.buildSectionElement(group);
- assert.isTrue(sectionEl.classList.contains('dueToRebase'));
- });
-
- test('creates first the removed and then the added rows', () => {
- const sectionEl = diffBuilder.buildSectionElement(group);
- const rowEls = sectionEl.querySelectorAll('.diff-row');
-
- assert.equal(rowEls.length, 4);
-
- assert.equal(
- rowEls[0].querySelector('.lineNum.left').textContent,
- lines[0].beforeNumber);
- assert.isNotOk(rowEls[0].querySelector('.lineNum.right'));
- assert.equal(
- rowEls[0].querySelector('.content').textContent, lines[0].text);
-
- assert.equal(
- rowEls[1].querySelector('.lineNum.left').textContent,
- lines[1].beforeNumber);
- assert.isNotOk(rowEls[1].querySelector('.lineNum.right'));
- assert.equal(
- rowEls[1].querySelector('.content').textContent, lines[1].text);
-
- assert.isNotOk(rowEls[2].querySelector('.lineNum.left'));
- assert.equal(
- rowEls[2].querySelector('.lineNum.right').textContent,
- lines[2].afterNumber);
- assert.equal(
- rowEls[2].querySelector('.content').textContent, lines[2].text);
-
- assert.isNotOk(rowEls[3].querySelector('.lineNum.left'));
- assert.equal(
- rowEls[3].querySelector('.lineNum.right').textContent,
- lines[3].afterNumber);
- assert.equal(
- rowEls[3].querySelector('.content').textContent, lines[3].text);
- });
-
- test('creates only the added rows if only ignored whitespace', () => {
- group.ignoredWhitespaceOnly = true;
- const sectionEl = diffBuilder.buildSectionElement(group);
- const rowEls = sectionEl.querySelectorAll('.diff-row');
-
- assert.equal(rowEls.length, 2);
-
- assert.isNotOk(rowEls[0].querySelector('.lineNum.left'));
- assert.equal(
- rowEls[0].querySelector('.lineNum.right').textContent,
- lines[2].afterNumber);
- assert.equal(
- rowEls[0].querySelector('.content').textContent, lines[2].text);
-
- assert.isNotOk(rowEls[1].querySelector('.lineNum.left'));
- assert.equal(
- rowEls[1].querySelector('.lineNum.right').textContent,
- lines[3].afterNumber);
- assert.equal(
- rowEls[1].querySelector('.content').textContent, lines[3].text);
- });
- });
-});
-
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-unified_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-unified_test.ts
new file mode 100644
index 0000000..7a9d06d
--- /dev/null
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-unified_test.ts
@@ -0,0 +1,282 @@
+/**
+ * @license
+ * Copyright 2016 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup-karma';
+import '../gr-diff/gr-diff-group';
+import './gr-diff-builder';
+import './gr-diff-builder-unified';
+import {GrDiffLine, GrDiffLineType} from '../gr-diff/gr-diff-line';
+import {GrDiffGroup, GrDiffGroupType} from '../gr-diff/gr-diff-group';
+import {GrDiffBuilderUnified} from './gr-diff-builder-unified';
+import {DiffPreferencesInfo} from '../../../api/diff';
+import {createDefaultDiffPrefs} from '../../../constants/constants';
+import {createDiff} from '../../../test/test-data-generators';
+import {queryAndAssert} from '../../../utils/common-util';
+
+suite('GrDiffBuilderUnified tests', () => {
+ let prefs: DiffPreferencesInfo;
+ let outputEl: HTMLElement;
+ let diffBuilder: GrDiffBuilderUnified;
+
+ setup(() => {
+ prefs = {
+ ...createDefaultDiffPrefs(),
+ line_length: 10,
+ show_tabs: true,
+ tab_size: 4,
+ };
+ outputEl = document.createElement('div');
+ diffBuilder = new GrDiffBuilderUnified(createDiff(), prefs, outputEl, []);
+ });
+
+ suite('buildSectionElement for BOTH group', () => {
+ let lines: GrDiffLine[];
+ let group: GrDiffGroup;
+
+ setup(() => {
+ lines = [
+ new GrDiffLine(GrDiffLineType.BOTH, 1, 2),
+ new GrDiffLine(GrDiffLineType.BOTH, 2, 3),
+ new GrDiffLine(GrDiffLineType.BOTH, 3, 4),
+ ];
+ lines[0].text = 'def hello_world():';
+ lines[1].text = ' print "Hello World";';
+ lines[2].text = ' return True';
+
+ group = new GrDiffGroup({type: GrDiffGroupType.BOTH, lines});
+ });
+
+ test('creates the section', () => {
+ const sectionEl = diffBuilder.buildSectionElement(group);
+ assert.isTrue(sectionEl.classList.contains('section'));
+ assert.isTrue(sectionEl.classList.contains('both'));
+ });
+
+ test('creates each unchanged row once', () => {
+ const sectionEl = diffBuilder.buildSectionElement(group);
+ const rowEls = sectionEl.querySelectorAll('.diff-row');
+
+ assert.equal(rowEls.length, 3);
+
+ assert.equal(
+ queryAndAssert(rowEls[0], '.lineNum.left').textContent,
+ lines[0].beforeNumber.toString()
+ );
+ assert.equal(
+ queryAndAssert(rowEls[0], '.lineNum.right').textContent,
+ lines[0].afterNumber.toString()
+ );
+ assert.equal(
+ queryAndAssert(rowEls[0], '.content').textContent,
+ lines[0].text
+ );
+
+ assert.equal(
+ queryAndAssert(rowEls[1], '.lineNum.left').textContent,
+ lines[1].beforeNumber.toString()
+ );
+ assert.equal(
+ queryAndAssert(rowEls[1], '.lineNum.right').textContent,
+ lines[1].afterNumber.toString()
+ );
+ assert.equal(
+ queryAndAssert(rowEls[1], '.content').textContent,
+ lines[1].text
+ );
+
+ assert.equal(
+ queryAndAssert(rowEls[2], '.lineNum.left').textContent,
+ lines[2].beforeNumber.toString()
+ );
+ assert.equal(
+ queryAndAssert(rowEls[2], '.lineNum.right').textContent,
+ lines[2].afterNumber.toString()
+ );
+ assert.equal(
+ queryAndAssert(rowEls[2], '.content').textContent,
+ lines[2].text
+ );
+ });
+ });
+
+ suite('buildSectionElement for moved chunks', () => {
+ test('creates a moved out group', () => {
+ const lines = [
+ new GrDiffLine(GrDiffLineType.REMOVE, 15),
+ new GrDiffLine(GrDiffLineType.REMOVE, 16),
+ ];
+ lines[0].text = 'def hello_world():';
+ lines[1].text = ' print "Hello World"';
+ const group = new GrDiffGroup({
+ type: GrDiffGroupType.DELTA,
+ lines,
+ moveDetails: {changed: false},
+ });
+
+ const sectionEl = diffBuilder.buildSectionElement(group);
+
+ const rowEls = sectionEl.querySelectorAll('tr');
+ const moveControlsRow = rowEls[0];
+ const cells = moveControlsRow.querySelectorAll('td');
+ assert.isTrue(sectionEl.classList.contains('dueToMove'));
+ assert.equal(rowEls.length, 3);
+ assert.isTrue(moveControlsRow.classList.contains('movedOut'));
+ assert.equal(cells.length, 3);
+ assert.isTrue(cells[2].classList.contains('moveHeader'));
+ assert.equal(cells[2].textContent, 'Moved out');
+ });
+
+ test('creates a moved in group', () => {
+ const lines = [
+ new GrDiffLine(GrDiffLineType.ADD, 37),
+ new GrDiffLine(GrDiffLineType.ADD, 38),
+ ];
+ lines[0].text = 'def hello_world():';
+ lines[1].text = ' print "Hello World"';
+ const group = new GrDiffGroup({
+ type: GrDiffGroupType.DELTA,
+ lines,
+ moveDetails: {changed: false},
+ });
+
+ const sectionEl = diffBuilder.buildSectionElement(group);
+
+ const rowEls = sectionEl.querySelectorAll('tr');
+ const moveControlsRow = rowEls[0];
+ const cells = moveControlsRow.querySelectorAll('td');
+ assert.isTrue(sectionEl.classList.contains('dueToMove'));
+ assert.equal(rowEls.length, 3);
+ assert.isTrue(moveControlsRow.classList.contains('movedIn'));
+ assert.equal(cells.length, 3);
+ assert.isTrue(cells[2].classList.contains('moveHeader'));
+ assert.equal(cells[2].textContent, 'Moved in');
+ });
+ });
+
+ suite('buildSectionElement for DELTA group', () => {
+ let lines: GrDiffLine[];
+ let group: GrDiffGroup;
+
+ setup(() => {
+ lines = [
+ new GrDiffLine(GrDiffLineType.REMOVE, 1),
+ new GrDiffLine(GrDiffLineType.REMOVE, 2),
+ new GrDiffLine(GrDiffLineType.ADD, 2),
+ new GrDiffLine(GrDiffLineType.ADD, 3),
+ ];
+ lines[0].text = 'def hello_world():';
+ lines[1].text = ' print "Hello World"';
+ lines[2].text = 'def hello_universe()';
+ lines[3].text = ' print "Hello Universe"';
+ });
+
+ test('creates the section', () => {
+ group = new GrDiffGroup({type: GrDiffGroupType.DELTA, lines});
+ const sectionEl = diffBuilder.buildSectionElement(group);
+ assert.isTrue(sectionEl.classList.contains('section'));
+ assert.isTrue(sectionEl.classList.contains('delta'));
+ });
+
+ test('creates the section with class if ignoredWhitespaceOnly', () => {
+ group = new GrDiffGroup({
+ type: GrDiffGroupType.DELTA,
+ lines,
+ ignoredWhitespaceOnly: true,
+ });
+ const sectionEl = diffBuilder.buildSectionElement(group);
+ assert.isTrue(sectionEl.classList.contains('ignoredWhitespaceOnly'));
+ });
+
+ test('creates the section with class if dueToRebase', () => {
+ group = new GrDiffGroup({
+ type: GrDiffGroupType.DELTA,
+ lines,
+ dueToRebase: true,
+ });
+ const sectionEl = diffBuilder.buildSectionElement(group);
+ assert.isTrue(sectionEl.classList.contains('dueToRebase'));
+ });
+
+ test('creates first the removed and then the added rows', () => {
+ group = new GrDiffGroup({type: GrDiffGroupType.DELTA, lines});
+ const sectionEl = diffBuilder.buildSectionElement(group);
+ const rowEls = sectionEl.querySelectorAll('.diff-row');
+
+ assert.equal(rowEls.length, 4);
+
+ assert.equal(
+ queryAndAssert(rowEls[0], '.lineNum.left').textContent,
+ lines[0].beforeNumber.toString()
+ );
+ assert.isNotOk(rowEls[0].querySelector('.lineNum.right'));
+ assert.equal(
+ queryAndAssert(rowEls[0], '.content').textContent,
+ lines[0].text
+ );
+
+ assert.equal(
+ queryAndAssert(rowEls[1], '.lineNum.left').textContent,
+ lines[1].beforeNumber.toString()
+ );
+ assert.isNotOk(rowEls[1].querySelector('.lineNum.right'));
+ assert.equal(
+ queryAndAssert(rowEls[1], '.content').textContent,
+ lines[1].text
+ );
+
+ assert.isNotOk(rowEls[2].querySelector('.lineNum.left'));
+ assert.equal(
+ queryAndAssert(rowEls[2], '.lineNum.right').textContent,
+ lines[2].afterNumber.toString()
+ );
+ assert.equal(
+ queryAndAssert(rowEls[2], '.content').textContent,
+ lines[2].text
+ );
+
+ assert.isNotOk(rowEls[3].querySelector('.lineNum.left'));
+ assert.equal(
+ queryAndAssert(rowEls[3], '.lineNum.right').textContent,
+ lines[3].afterNumber.toString()
+ );
+ assert.equal(
+ queryAndAssert(rowEls[3], '.content').textContent,
+ lines[3].text
+ );
+ });
+
+ test('creates only the added rows if only ignored whitespace', () => {
+ group = new GrDiffGroup({
+ type: GrDiffGroupType.DELTA,
+ lines,
+ ignoredWhitespaceOnly: true,
+ });
+ const sectionEl = diffBuilder.buildSectionElement(group);
+ const rowEls = sectionEl.querySelectorAll('.diff-row');
+
+ assert.equal(rowEls.length, 2);
+
+ assert.isNotOk(rowEls[0].querySelector('.lineNum.left'));
+ assert.equal(
+ queryAndAssert(rowEls[0], '.lineNum.right').textContent,
+ lines[2].afterNumber.toString()
+ );
+ assert.equal(
+ queryAndAssert(rowEls[0], '.content').textContent,
+ lines[2].text
+ );
+
+ assert.isNotOk(rowEls[1].querySelector('.lineNum.left'));
+ assert.equal(
+ queryAndAssert(rowEls[1], '.lineNum.right').textContent,
+ lines[3].afterNumber.toString()
+ );
+ assert.equal(
+ queryAndAssert(rowEls[1], '.content').textContent,
+ lines[3].text
+ );
+ });
+ });
+});
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/embed/diff/gr-diff-cursor/gr-diff-cursor.ts b/polygerrit-ui/app/embed/diff/gr-diff-cursor/gr-diff-cursor.ts
index dfe8a15..e80d86b 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-cursor/gr-diff-cursor.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-cursor/gr-diff-cursor.ts
@@ -109,7 +109,8 @@
*/
initialLineNumber: number | null = null;
- private cursorManager = new GrCursorManager();
+ // visible for testing
+ cursorManager = new GrCursorManager();
private targetSubscription?: Subscription;
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-cursor/gr-diff-cursor_test.js b/polygerrit-ui/app/embed/diff/gr-diff-cursor/gr-diff-cursor_test.js
deleted file mode 100644
index f48d673..0000000
--- a/polygerrit-ui/app/embed/diff/gr-diff-cursor/gr-diff-cursor_test.js
+++ /dev/null
@@ -1,681 +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 '../gr-diff/gr-diff.js';
-import './gr-diff-cursor.js';
-import {fixture, html} from '@open-wc/testing-helpers';
-import {listenOnce, mockPromise} from '../../../test/test-utils.js';
-import {createDiff} from '../../../test/test-data-generators.js';
-import {createDefaultDiffPrefs} from '../../../constants/constants.js';
-import {GrDiffCursor} from './gr-diff-cursor.js';
-import {afterNextRender} from '@polymer/polymer/lib/utils/render-status';
-
-suite('gr-diff-cursor tests', () => {
- let cursor;
- let diffElement;
- let diff;
-
- setup(async () => {
- diffElement = await fixture(html`<gr-diff></gr-diff>`);
- cursor = new GrDiffCursor();
-
- // Register the diff with the cursor.
- cursor.replaceDiffs([diffElement]);
-
- diffElement.loggedIn = false;
- diffElement.comments = {
- left: [],
- right: [],
- meta: {},
- };
- diffElement.path = 'some/path.ts';
- const promise = mockPromise();
- const setupDone = () => {
- cursor._updateStops();
- cursor.moveToFirstChunk();
- diffElement.removeEventListener('render', setupDone);
- promise.resolve();
- };
- diffElement.addEventListener('render', setupDone);
-
- diff = createDiff();
- diffElement.prefs = createDefaultDiffPrefs();
- diffElement.diff = diff;
- await promise;
- });
-
- test('diff cursor functionality (side-by-side)', () => {
- // The cursor has been initialized to the first delta.
- assert.isOk(cursor.diffRow);
-
- const firstDeltaRow = diffElement.shadowRoot
- .querySelector('.section.delta .diff-row');
- assert.equal(cursor.diffRow, firstDeltaRow);
-
- cursor.moveDown();
-
- assert.notEqual(cursor.diffRow, firstDeltaRow);
- assert.equal(cursor.diffRow, firstDeltaRow.nextSibling);
-
- cursor.moveUp();
-
- assert.notEqual(cursor.diffRow, firstDeltaRow.nextSibling);
- assert.equal(cursor.diffRow, firstDeltaRow);
- });
-
- test('moveToFirstChunk', async () => {
- const diff = {
- meta_a: {
- name: 'lorem-ipsum.txt',
- content_type: 'text/plain',
- lines: 3,
- },
- meta_b: {
- name: 'lorem-ipsum.txt',
- content_type: 'text/plain',
- lines: 3,
- },
- intraline_status: 'OK',
- change_type: 'MODIFIED',
- diff_header: [
- 'diff --git a/lorem-ipsum.txt b/lorem-ipsum.txt',
- 'index b2adcf4..554ae49 100644',
- '--- a/lorem-ipsum.txt',
- '+++ b/lorem-ipsum.txt',
- ],
- content: [
- {b: ['new line 1']},
- {ab: ['unchanged line']},
- {a: ['old line 2']},
- {ab: ['more unchanged lines']},
- ],
- };
-
- diffElement.diff = diff;
- // The file comment button, if present, is a cursor stop. Ensure
- // moveToFirstChunk() works correctly even if the button is not shown.
- diffElement.prefs.show_file_comment_button = false;
- await flush();
- cursor._updateStops();
-
- const chunks = Array.from(diffElement.root.querySelectorAll(
- '.section.delta'));
- assert.equal(chunks.length, 2);
-
- // Verify it works on fresh diff.
- cursor.moveToFirstChunk();
- assert.equal(chunks.indexOf(cursor.diffRow.parentElement), 0);
- assert.equal(cursor.side, 'right');
-
- // Verify it works from other cursor positions.
- cursor.moveToNextChunk();
- assert.equal(chunks.indexOf(cursor.diffRow.parentElement), 1);
- assert.equal(cursor.side, 'left');
- cursor.moveToFirstChunk();
- assert.equal(chunks.indexOf(cursor.diffRow.parentElement), 0);
- assert.equal(cursor.side, 'right');
- });
-
- test('moveToLastChunk', async () => {
- const diff = {
- meta_a: {
- name: 'lorem-ipsum.txt',
- content_type: 'text/plain',
- lines: 3,
- },
- meta_b: {
- name: 'lorem-ipsum.txt',
- content_type: 'text/plain',
- lines: 3,
- },
- intraline_status: 'OK',
- change_type: 'MODIFIED',
- diff_header: [
- 'diff --git a/lorem-ipsum.txt b/lorem-ipsum.txt',
- 'index b2adcf4..554ae49 100644',
- '--- a/lorem-ipsum.txt',
- '+++ b/lorem-ipsum.txt',
- ],
- content: [
- {ab: ['unchanged line']},
- {a: ['old line 2']},
- {ab: ['more unchanged lines']},
- {b: ['new line 3']},
- ],
- };
-
- diffElement.diff = diff;
- await new Promise(resolve => afterNextRender(diffElement, resolve));
- cursor._updateStops();
-
- const chunks = Array.from(diffElement.root.querySelectorAll(
- '.section.delta'));
- assert.equal(chunks.length, 2);
-
- // Verify it works on fresh diff.
- cursor.moveToLastChunk();
- assert.equal(chunks.indexOf(cursor.diffRow.parentElement), 1);
- assert.equal(cursor.side, 'right');
-
- // Verify it works from other cursor positions.
- cursor.moveToPreviousChunk();
- assert.equal(chunks.indexOf(cursor.diffRow.parentElement), 0);
- assert.equal(cursor.side, 'left');
- cursor.moveToLastChunk();
- assert.equal(chunks.indexOf(cursor.diffRow.parentElement), 1);
- assert.equal(cursor.side, 'right');
- });
-
- test('cursor scroll behavior', () => {
- assert.equal(cursor.cursorManager.scrollMode, 'keep-visible');
-
- diffElement.dispatchEvent(new Event('render-start'));
- assert.isTrue(cursor.cursorManager.focusOnMove);
-
- window.dispatchEvent(new Event('scroll'));
- assert.equal(cursor.cursorManager.scrollMode, 'never');
- assert.isFalse(cursor.cursorManager.focusOnMove);
-
- diffElement.dispatchEvent(new Event('render-content'));
- assert.isTrue(cursor.cursorManager.focusOnMove);
-
- cursor.reInitCursor();
- assert.equal(cursor.cursorManager.scrollMode, 'keep-visible');
- });
-
- test('moves to selected line', () => {
- const moveToNumStub = sinon.stub(cursor, 'moveToLineNumber');
-
- diffElement.dispatchEvent(
- new CustomEvent('line-selected', {
- detail: {number: '123', side: 'right', path: 'some/file'},
- }));
-
- assert.isTrue(moveToNumStub.called);
- assert.equal(moveToNumStub.lastCall.args[0], '123');
- assert.equal(moveToNumStub.lastCall.args[1], 'right');
- assert.equal(moveToNumStub.lastCall.args[2], 'some/file');
- });
-
- suite('unified diff', () => {
- setup(async () => {
- diffElement.viewMode = 'UNIFIED_DIFF';
- // We must allow the diff to re-render after setting the viewMode.
- await new Promise(resolve => afterNextRender(diffElement, resolve));
- cursor.reInitCursor();
- });
-
- test('diff cursor functionality (unified)', () => {
- // The cursor has been initialized to the first delta.
- assert.isOk(cursor.diffRow);
-
- let firstDeltaRow = diffElement.shadowRoot
- .querySelector('.section.delta .diff-row');
- assert.equal(cursor.diffRow, firstDeltaRow);
-
- firstDeltaRow = diffElement.shadowRoot
- .querySelector('.section.delta .diff-row');
- assert.equal(cursor.diffRow, firstDeltaRow);
-
- cursor.moveDown();
-
- assert.notEqual(cursor.diffRow, firstDeltaRow);
- assert.equal(cursor.diffRow, firstDeltaRow.nextSibling);
-
- cursor.moveUp();
-
- assert.notEqual(cursor.diffRow, firstDeltaRow.nextSibling);
- assert.equal(cursor.diffRow, firstDeltaRow);
- });
- });
-
- test('cursor side functionality', () => {
- // The side only applies to side-by-side mode, which should be the default
- // mode.
- assert.equal(diffElement.viewMode, 'SIDE_BY_SIDE');
-
- const firstDeltaSection = diffElement.shadowRoot
- .querySelector('.section.delta');
- const firstDeltaRow = firstDeltaSection.querySelector('.diff-row');
-
- // Because the first delta in this diff is on the right, it should be set
- // to the right side.
- assert.equal(cursor.side, 'right');
- assert.equal(cursor.diffRow, firstDeltaRow);
- const firstIndex = cursor.cursorManager.index;
-
- // Move the side to the left. Because this delta only has a right side, we
- // should be moved up to the previous line where there is content on the
- // right. The previous row is part of the previous section.
- cursor.moveLeft();
-
- assert.equal(cursor.side, 'left');
- assert.notEqual(cursor.diffRow, firstDeltaRow);
- assert.equal(cursor.cursorManager.index, firstIndex - 1);
- assert.equal(cursor.diffRow.parentElement,
- firstDeltaSection.previousSibling);
-
- // If we move down, we should skip everything in the first delta because
- // we are on the left side and the first delta has no content on the left.
- cursor.moveDown();
-
- assert.equal(cursor.side, 'left');
- assert.notEqual(cursor.diffRow, firstDeltaRow);
- assert.isTrue(cursor.cursorManager.index > firstIndex);
- assert.equal(cursor.diffRow.parentElement,
- firstDeltaSection.nextSibling);
- });
-
- test('chunk skip functionality', () => {
- const chunks = diffElement.root.querySelectorAll(
- '.section.delta');
- const indexOfChunk = function(chunk) {
- return Array.prototype.indexOf.call(chunks, chunk);
- };
-
- // We should be initialized to the first chunk. Since this chunk only has
- // content on the right side, our side should be right.
- let currentIndex = indexOfChunk(cursor.diffRow.parentElement);
- assert.equal(currentIndex, 0);
- assert.equal(cursor.side, 'right');
-
- // Move to the next chunk.
- cursor.moveToNextChunk();
-
- // Since this chunk only has content on the left side. we should have been
- // automatically moved over.
- const previousIndex = currentIndex;
- currentIndex = indexOfChunk(cursor.diffRow.parentElement);
- assert.equal(currentIndex, previousIndex + 1);
- assert.equal(cursor.side, 'left');
- });
-
- suite('moved chunks without line range)', () => {
- setup(async () => {
- const promise = mockPromise();
- const renderHandler = function() {
- diffElement.removeEventListener('render', renderHandler);
- cursor.reInitCursor();
- promise.resolve();
- };
- diffElement.addEventListener('render', renderHandler);
- diffElement.diff = {...diff, content: [
- {
- ab: [
- 'Lorem ipsum dolor sit amet, suspendisse inceptos vehicula, ',
- ],
- },
- {
- b: [
- 'Nullam neque, ligula ac, id blandit.',
- 'Sagittis tincidunt torquent, tempor nunc amet.',
- 'At rhoncus id.',
- ],
- move_details: {changed: false},
- },
- {
- ab: [
- 'Sem nascetur, erat ut, non in.',
- ],
- },
- {
- a: [
- 'Nullam neque, ligula ac, id blandit.',
- 'Sagittis tincidunt torquent, tempor nunc amet.',
- 'At rhoncus id.',
- ],
- move_details: {changed: false},
- },
- {
- ab: [
- 'Arcu eget, rhoncus amet cursus, ipsum elementum.',
- ],
- },
- ]};
- await promise;
- });
-
- test('renders moveControls with simple descriptions', () => {
- const [movedIn, movedOut] = diffElement.root
- .querySelectorAll('.dueToMove .moveControls');
- assert.equal(movedIn.textContent, 'Moved in');
- assert.equal(movedOut.textContent, 'Moved out');
- });
- });
-
- suite('moved chunks (moveDetails)', () => {
- setup(async () => {
- const promise = mockPromise();
- const renderHandler = function() {
- diffElement.removeEventListener('render', renderHandler);
- cursor.reInitCursor();
- promise.resolve();
- };
- diffElement.addEventListener('render', renderHandler);
- diffElement.diff = {...diff, content: [
- {
- ab: [
- 'Lorem ipsum dolor sit amet, suspendisse inceptos vehicula, ',
- ],
- },
- {
- b: [
- 'Nullam neque, ligula ac, id blandit.',
- 'Sagittis tincidunt torquent, tempor nunc amet.',
- 'At rhoncus id.',
- ],
- move_details: {changed: false, range: {start: 4, end: 6}},
- },
- {
- ab: [
- 'Sem nascetur, erat ut, non in.',
- ],
- },
- {
- a: [
- 'Nullam neque, ligula ac, id blandit.',
- 'Sagittis tincidunt torquent, tempor nunc amet.',
- 'At rhoncus id.',
- ],
- move_details: {changed: false, range: {start: 2, end: 4}},
- },
- {
- ab: [
- 'Arcu eget, rhoncus amet cursus, ipsum elementum.',
- ],
- },
- ]};
- await promise;
- });
-
- test('renders moveControls with simple descriptions', () => {
- const [movedIn, movedOut] = diffElement.root
- .querySelectorAll('.dueToMove .moveControls');
- assert.equal(movedIn.textContent, 'Moved from lines 4 - 6');
- assert.equal(movedOut.textContent, 'Moved to lines 2 - 4');
- });
-
- test('startLineAnchor of movedIn chunk fires events', async () => {
- const [movedIn] = diffElement.root
- .querySelectorAll('.dueToMove .moveControls');
- const [startLineAnchor] = movedIn.querySelectorAll('a');
-
- const promise = mockPromise();
- const onMovedLinkClicked = e => {
- assert.deepEqual(e.detail, {lineNum: 4, side: 'left'});
- promise.resolve();
- };
- assert.equal(startLineAnchor.textContent, '4');
- startLineAnchor
- .addEventListener('moved-link-clicked', onMovedLinkClicked);
- MockInteractions.click(startLineAnchor);
- await promise;
- });
-
- test('endLineAnchor of movedOut fires events', async () => {
- const [, movedOut] = diffElement.root
- .querySelectorAll('.dueToMove .moveControls');
- const [, endLineAnchor] = movedOut.querySelectorAll('a');
-
- const promise = mockPromise();
- const onMovedLinkClicked = e => {
- assert.deepEqual(e.detail, {lineNum: 4, side: 'right'});
- promise.resolve();
- };
- assert.equal(endLineAnchor.textContent, '4');
- endLineAnchor.addEventListener('moved-link-clicked', onMovedLinkClicked);
- MockInteractions.click(endLineAnchor);
- await promise;
- });
- });
-
- test('initialLineNumber not provided', async () => {
- let scrollBehaviorDuringMove;
- const moveToNumStub = sinon.stub(cursor, 'moveToLineNumber');
- const moveToChunkStub = sinon.stub(cursor, 'moveToFirstChunk')
- .callsFake(() => {
- scrollBehaviorDuringMove = cursor.cursorManager.scrollMode;
- });
-
- diffElement._diffChanged(createDiff());
- await new Promise(resolve => afterNextRender(diffElement, resolve));
- cursor.reInitCursor();
- assert.isFalse(moveToNumStub.called);
- assert.isTrue(moveToChunkStub.called);
- assert.equal(scrollBehaviorDuringMove, 'never');
- assert.equal(cursor.cursorManager.scrollMode, 'keep-visible');
- });
-
- test('initialLineNumber provided', async () => {
- let scrollBehaviorDuringMove;
- const moveToNumStub = sinon.stub(cursor, 'moveToLineNumber')
- .callsFake(() => {
- scrollBehaviorDuringMove = cursor.cursorManager.scrollMode;
- });
- const moveToChunkStub = sinon.stub(cursor, 'moveToFirstChunk');
- cursor.initialLineNumber = 10;
- cursor.side = 'right';
-
- diffElement._diffChanged(createDiff());
- await new Promise(resolve => afterNextRender(diffElement, resolve));
- cursor.reInitCursor();
- assert.isFalse(moveToChunkStub.called);
- assert.isTrue(moveToNumStub.called);
- assert.equal(moveToNumStub.lastCall.args[0], 10);
- assert.equal(moveToNumStub.lastCall.args[1], 'right');
- assert.equal(scrollBehaviorDuringMove, 'keep-visible');
- assert.equal(cursor.cursorManager.scrollMode, 'keep-visible');
- });
-
- test('getTargetDiffElement', () => {
- cursor.initialLineNumber = 1;
- assert.isTrue(!!cursor.diffRow);
- assert.equal(
- cursor.getTargetDiffElement(),
- diffElement
- );
- });
-
- suite('createCommentInPlace', () => {
- setup(() => {
- diffElement.loggedIn = true;
- });
-
- test('adds new draft for selected line on the left', async () => {
- cursor.moveToLineNumber(2, 'left');
- const promise = mockPromise();
- diffElement.addEventListener('create-comment', e => {
- const {lineNum, range, side} = e.detail;
- assert.equal(lineNum, 2);
- assert.equal(range, undefined);
- assert.equal(side, 'left');
- promise.resolve();
- });
- cursor.createCommentInPlace();
- await promise;
- });
-
- test('adds draft for selected line on the right', async () => {
- cursor.moveToLineNumber(4, 'right');
- const promise = mockPromise();
- diffElement.addEventListener('create-comment', e => {
- const {lineNum, range, side} = e.detail;
- assert.equal(lineNum, 4);
- assert.equal(range, undefined);
- assert.equal(side, 'right');
- promise.resolve();
- });
- cursor.createCommentInPlace();
- await promise;
- });
-
- test('creates comment for range if selected', async () => {
- const someRange = {
- start_line: 2,
- start_character: 3,
- end_line: 6,
- end_character: 1,
- };
- diffElement.highlights.selectedRange = {
- side: 'right',
- range: someRange,
- };
- const promise = mockPromise();
- diffElement.addEventListener('create-comment', e => {
- const {lineNum, range, side} = e.detail;
- assert.equal(lineNum, 6);
- assert.equal(range, someRange);
- assert.equal(side, 'right');
- promise.resolve();
- });
- cursor.createCommentInPlace();
- await promise;
- });
-
- test('ignores call if nothing is selected', () => {
- const createRangeCommentStub = sinon.stub(diffElement,
- 'createRangeComment');
- const addDraftAtLineStub = sinon.stub(diffElement, 'addDraftAtLine');
- cursor.diffRow = undefined;
- cursor.createCommentInPlace();
- assert.isFalse(createRangeCommentStub.called);
- assert.isFalse(addDraftAtLineStub.called);
- });
- });
-
- test('getAddress', () => {
- // It should initialize to the first chunk: line 5 of the revision.
- assert.deepEqual(cursor.getAddress(),
- {leftSide: false, number: 5});
-
- // Revision line 4 is up.
- cursor.moveUp();
- assert.deepEqual(cursor.getAddress(),
- {leftSide: false, number: 4});
-
- // Base line 4 is left.
- cursor.moveLeft();
- assert.deepEqual(cursor.getAddress(), {leftSide: true, number: 4});
-
- // Moving to the next chunk takes it back to the start.
- cursor.moveToNextChunk();
- assert.deepEqual(cursor.getAddress(),
- {leftSide: false, number: 5});
-
- // The following chunk is a removal starting on line 10 of the base.
- cursor.moveToNextChunk();
- assert.deepEqual(cursor.getAddress(),
- {leftSide: true, number: 10});
-
- // Should be null if there is no selection.
- cursor.cursorManager.unsetCursor();
- assert.isNotOk(cursor.getAddress());
- });
-
- test('_findRowByNumberAndFile', () => {
- // Get the first ab row after the first chunk.
- const row = diffElement.root.querySelectorAll('tr')[9];
-
- // It should be line 8 on the right, but line 5 on the left.
- assert.equal(cursor._findRowByNumberAndFile(8, 'right'), row);
- assert.equal(cursor._findRowByNumberAndFile(5, 'left'), row);
- });
-
- test('expand context updates stops', async () => {
- sinon.spy(cursor, '_updateStops');
- MockInteractions.tap(diffElement.shadowRoot
- .querySelector('gr-context-controls').shadowRoot
- .querySelector('.showContext'));
- await new Promise(resolve => afterNextRender(diffElement, resolve));
- assert.isTrue(cursor._updateStops.called);
- });
-
- test('updates stops when loading changes', () => {
- sinon.spy(cursor, '_updateStops');
- diffElement.dispatchEvent(new Event('loading-changed'));
- assert.isTrue(cursor._updateStops.called);
- });
-
- suite('multi diff', () => {
- let diffElements;
-
- setup(async () => {
- diffElements = [
- await fixture(html`<gr-diff></gr-diff>`),
- await fixture(html`<gr-diff></gr-diff>`),
- await fixture(html`<gr-diff></gr-diff>`),
- ];
- cursor = new GrDiffCursor();
-
- // Register the diff with the cursor.
- cursor.replaceDiffs(diffElements);
-
- for (const el of diffElements) {
- el.prefs = createDefaultDiffPrefs();
- }
- });
-
- function getTargetDiffIndex() {
- // Mocha has a bug where when `assert.equals` fails, it will try to
- // JSON.stringify the operands, which fails when they are cyclic structures
- // like GrDiffElement. The failure is difficult to attribute to a specific
- // assertion because of the async nature assertion errors are handled and
- // can cause the test simply timing out, causing a lot of debugging headache.
- // Working with indices circumvents the problem.
- return diffElements.indexOf(cursor.getTargetDiffElement());
- }
-
- test('do not skip loading diffs', async () => {
- const diffRenderedPromises =
- diffElements.map(diffEl => listenOnce(diffEl, 'render'));
-
- diffElements[0].diff = createDiff();
- diffElements[2].diff = createDiff();
- await Promise.all([diffRenderedPromises[0], diffRenderedPromises[2]]);
- await new Promise(resolve => afterNextRender(diffElements[0], resolve));
-
- const lastLine = diffElements[0].diff.meta_b.lines;
-
- // Goto second last line of the first diff
- cursor.moveToLineNumber(lastLine - 1, 'right');
- assert.equal(
- cursor.getTargetLineElement().textContent, lastLine - 1);
-
- // Can move down until we reach the loading file
- cursor.moveDown();
- assert.equal(getTargetDiffIndex(), 0);
- assert.equal(cursor.getTargetLineElement().textContent, lastLine);
-
- // Cannot move down while still loading the diff we would switch to
- cursor.moveDown();
- assert.equal(getTargetDiffIndex(), 0);
- assert.equal(cursor.getTargetLineElement().textContent, lastLine);
-
- // Diff 1 finishing to load
- diffElements[1].diff = createDiff();
- await diffRenderedPromises[1];
- await new Promise(resolve => afterNextRender(diffElements[0], resolve));
-
- // Now we can go down
- cursor.moveDown();
- assert.equal(getTargetDiffIndex(), 1);
- assert.equal(cursor.getTargetLineElement().textContent, 'File');
- });
- });
-});
-
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-cursor/gr-diff-cursor_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-cursor/gr-diff-cursor_test.ts
new file mode 100644
index 0000000..ac9b407
--- /dev/null
+++ b/polygerrit-ui/app/embed/diff/gr-diff-cursor/gr-diff-cursor_test.ts
@@ -0,0 +1,693 @@
+/**
+ * @license
+ * Copyright 2016 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup-karma';
+import '../gr-diff/gr-diff';
+import './gr-diff-cursor';
+import {fixture, html} from '@open-wc/testing-helpers';
+import {mockPromise, queryAll, queryAndAssert} from '../../../test/test-utils';
+import {createDiff} from '../../../test/test-data-generators';
+import {createDefaultDiffPrefs} from '../../../constants/constants';
+import {GrDiffCursor} from './gr-diff-cursor';
+import {waitForEventOnce} from '../../../utils/event-util';
+import {DiffInfo, DiffViewMode, Side} from '../../../api/diff';
+import {GrDiff} from '../gr-diff/gr-diff';
+import {assertIsDefined} from '../../../utils/common-util';
+
+suite('gr-diff-cursor tests', () => {
+ let cursor: GrDiffCursor;
+ let diffElement: GrDiff;
+ let diff: DiffInfo;
+
+ setup(async () => {
+ diffElement = await fixture(html`<gr-diff></gr-diff>`);
+ cursor = new GrDiffCursor();
+
+ // Register the diff with the cursor.
+ cursor.replaceDiffs([diffElement]);
+
+ diffElement.loggedIn = false;
+ diffElement.path = 'some/path.ts';
+ const promise = mockPromise();
+ const setupDone = () => {
+ cursor._updateStops();
+ cursor.moveToFirstChunk();
+ diffElement.removeEventListener('render', setupDone);
+ promise.resolve();
+ };
+ diffElement.addEventListener('render', setupDone);
+
+ diff = createDiff();
+ diffElement.prefs = createDefaultDiffPrefs();
+ diffElement.diff = diff;
+ await promise;
+ });
+
+ test('diff cursor functionality (side-by-side)', () => {
+ // The cursor has been initialized to the first delta.
+ assert.isOk(cursor.diffRow);
+
+ const firstDeltaRow = queryAndAssert<HTMLElement>(
+ diffElement,
+ '.section.delta .diff-row'
+ );
+ assert.equal(cursor.diffRow, firstDeltaRow);
+
+ cursor.moveDown();
+
+ assert.isOk(firstDeltaRow.nextElementSibling);
+ assert.notEqual(cursor.diffRow, firstDeltaRow);
+ assert.equal(
+ cursor.diffRow,
+ firstDeltaRow.nextElementSibling as HTMLElement
+ );
+
+ cursor.moveUp();
+
+ assert.isOk(firstDeltaRow.nextElementSibling);
+ assert.notEqual(
+ cursor.diffRow,
+ firstDeltaRow.nextElementSibling as HTMLElement
+ );
+ assert.equal(cursor.diffRow, firstDeltaRow);
+ });
+
+ test('moveToFirstChunk', async () => {
+ const diff: DiffInfo = {
+ meta_a: {
+ name: 'lorem-ipsum.txt',
+ content_type: 'text/plain',
+ lines: 3,
+ },
+ meta_b: {
+ name: 'lorem-ipsum.txt',
+ content_type: 'text/plain',
+ lines: 3,
+ },
+ intraline_status: 'OK',
+ change_type: 'MODIFIED',
+ diff_header: [
+ 'diff --git a/lorem-ipsum.txt b/lorem-ipsum.txt',
+ 'index b2adcf4..554ae49 100644',
+ '--- a/lorem-ipsum.txt',
+ '+++ b/lorem-ipsum.txt',
+ ],
+ content: [
+ {b: ['new line 1']},
+ {ab: ['unchanged line']},
+ {a: ['old line 2']},
+ {ab: ['more unchanged lines']},
+ ],
+ };
+
+ diffElement.diff = diff;
+ // The file comment button, if present, is a cursor stop. Ensure
+ // moveToFirstChunk() works correctly even if the button is not shown.
+ diffElement.prefs!.show_file_comment_button = false;
+ await waitForEventOnce(diffElement, 'render');
+
+ cursor._updateStops();
+
+ const chunks = [
+ ...queryAll(diffElement, '.section.delta'),
+ ] as HTMLElement[];
+ assert.equal(chunks.length, 2);
+
+ // Verify it works on fresh diff.
+ cursor.moveToFirstChunk();
+ assert.ok(cursor.diffRow);
+ assert.equal(chunks.indexOf(cursor.diffRow!.parentElement!), 0);
+ assert.equal(cursor.side, Side.RIGHT);
+
+ // Verify it works from other cursor positions.
+ cursor.moveToNextChunk();
+ assert.ok(cursor.diffRow);
+ assert.equal(chunks.indexOf(cursor.diffRow!.parentElement!), 1);
+ assert.equal(cursor.side, Side.LEFT);
+ cursor.moveToFirstChunk();
+ assert.ok(cursor.diffRow);
+ assert.equal(chunks.indexOf(cursor.diffRow!.parentElement!), 0);
+ assert.equal(cursor.side, Side.RIGHT);
+ });
+
+ test('moveToLastChunk', async () => {
+ const diff: DiffInfo = {
+ meta_a: {
+ name: 'lorem-ipsum.txt',
+ content_type: 'text/plain',
+ lines: 3,
+ },
+ meta_b: {
+ name: 'lorem-ipsum.txt',
+ content_type: 'text/plain',
+ lines: 3,
+ },
+ intraline_status: 'OK',
+ change_type: 'MODIFIED',
+ diff_header: [
+ 'diff --git a/lorem-ipsum.txt b/lorem-ipsum.txt',
+ 'index b2adcf4..554ae49 100644',
+ '--- a/lorem-ipsum.txt',
+ '+++ b/lorem-ipsum.txt',
+ ],
+ content: [
+ {ab: ['unchanged line']},
+ {a: ['old line 2']},
+ {ab: ['more unchanged lines']},
+ {b: ['new line 3']},
+ ],
+ };
+
+ diffElement.diff = diff;
+ await waitForEventOnce(diffElement, 'render');
+ cursor._updateStops();
+
+ const chunks = [...queryAll(diffElement, '.section.delta')];
+ assert.equal(chunks.length, 2);
+
+ // Verify it works on fresh diff.
+ cursor.moveToLastChunk();
+ assert.equal(chunks.indexOf(cursor.diffRow!.parentElement!), 1);
+ assert.equal(cursor.side, Side.RIGHT);
+
+ // Verify it works from other cursor positions.
+ cursor.moveToPreviousChunk();
+ assert.equal(chunks.indexOf(cursor.diffRow!.parentElement!), 0);
+ assert.equal(cursor.side, Side.LEFT);
+ cursor.moveToLastChunk();
+ assert.equal(chunks.indexOf(cursor.diffRow!.parentElement!), 1);
+ assert.equal(cursor.side, Side.RIGHT);
+ });
+
+ test('cursor scroll behavior', () => {
+ assert.equal(cursor.cursorManager.scrollMode, 'keep-visible');
+
+ diffElement.dispatchEvent(new Event('render-start'));
+ assert.isTrue(cursor.cursorManager.focusOnMove);
+
+ window.dispatchEvent(new Event('scroll'));
+ assert.equal(cursor.cursorManager.scrollMode, 'never');
+ assert.isFalse(cursor.cursorManager.focusOnMove);
+
+ diffElement.dispatchEvent(new Event('render-content'));
+ assert.isTrue(cursor.cursorManager.focusOnMove);
+
+ cursor.reInitCursor();
+ assert.equal(cursor.cursorManager.scrollMode, 'keep-visible');
+ });
+
+ test('moves to selected line', () => {
+ const moveToNumStub = sinon.stub(cursor, 'moveToLineNumber');
+
+ diffElement.dispatchEvent(
+ new CustomEvent('line-selected', {
+ detail: {number: '123', side: Side.RIGHT, path: 'some/file'},
+ })
+ );
+
+ assert.isTrue(moveToNumStub.called);
+ assert.equal(moveToNumStub.lastCall.args[0], 123);
+ assert.equal(moveToNumStub.lastCall.args[1], Side.RIGHT);
+ assert.equal(moveToNumStub.lastCall.args[2], 'some/file');
+ });
+
+ suite('unified diff', () => {
+ setup(async () => {
+ diffElement.viewMode = DiffViewMode.UNIFIED;
+ await waitForEventOnce(diffElement, 'render');
+ cursor.reInitCursor();
+ });
+
+ test('diff cursor functionality (unified)', () => {
+ // The cursor has been initialized to the first delta.
+ assert.isOk(cursor.diffRow);
+
+ const firstDeltaRow = queryAndAssert<HTMLElement>(
+ diffElement,
+ '.section.delta .diff-row'
+ );
+ assert.equal(cursor.diffRow, firstDeltaRow);
+
+ cursor.moveDown();
+
+ assert.notEqual(cursor.diffRow, firstDeltaRow);
+ assert.equal(
+ cursor.diffRow,
+ firstDeltaRow.nextElementSibling as HTMLElement
+ );
+
+ cursor.moveUp();
+
+ assert.notEqual(
+ cursor.diffRow,
+ firstDeltaRow.nextElementSibling as HTMLElement
+ );
+ assert.equal(cursor.diffRow, firstDeltaRow);
+ });
+ });
+
+ test('cursor side functionality', () => {
+ // The side only applies to side-by-side mode, which should be the default
+ // mode.
+ assert.equal(diffElement.viewMode, 'SIDE_BY_SIDE');
+
+ const firstDeltaSection = queryAndAssert<HTMLElement>(
+ diffElement,
+ '.section.delta'
+ );
+ const firstDeltaRow = queryAndAssert<HTMLElement>(
+ firstDeltaSection,
+ '.diff-row'
+ );
+
+ // Because the first delta in this diff is on the right, it should be set
+ // to the right side.
+ assert.equal(cursor.side, Side.RIGHT);
+ assert.equal(cursor.diffRow, firstDeltaRow);
+ const firstIndex = cursor.cursorManager.index;
+
+ // Move the side to the left. Because this delta only has a right side, we
+ // should be moved up to the previous line where there is content on the
+ // right. The previous row is part of the previous section.
+ cursor.moveLeft();
+
+ assert.equal(cursor.side, Side.LEFT);
+ assert.notEqual(cursor.diffRow, firstDeltaRow);
+ assert.equal(cursor.cursorManager.index, firstIndex - 1);
+ assert.equal(
+ cursor.diffRow!.parentElement,
+ firstDeltaSection.previousSibling
+ );
+
+ // If we move down, we should skip everything in the first delta because
+ // we are on the left side and the first delta has no content on the left.
+ cursor.moveDown();
+
+ assert.equal(cursor.side, Side.LEFT);
+ assert.notEqual(cursor.diffRow, firstDeltaRow);
+ assert.isTrue(cursor.cursorManager.index > firstIndex);
+ assert.equal(cursor.diffRow!.parentElement, firstDeltaSection.nextSibling);
+ });
+
+ test('chunk skip functionality', () => {
+ const chunks = [...queryAll(diffElement, '.section.delta')];
+ const indexOfChunk = function (chunk: HTMLElement) {
+ return Array.prototype.indexOf.call(chunks, chunk);
+ };
+
+ // We should be initialized to the first chunk. Since this chunk only has
+ // content on the right side, our side should be right.
+ let currentIndex = indexOfChunk(cursor.diffRow!.parentElement!);
+ assert.equal(currentIndex, 0);
+ assert.equal(cursor.side, Side.RIGHT);
+
+ // Move to the next chunk.
+ cursor.moveToNextChunk();
+
+ // Since this chunk only has content on the left side. we should have been
+ // automatically moved over.
+ const previousIndex = currentIndex;
+ currentIndex = indexOfChunk(cursor.diffRow!.parentElement!);
+ assert.equal(currentIndex, previousIndex + 1);
+ assert.equal(cursor.side, Side.LEFT);
+ });
+
+ suite('moved chunks without line range)', () => {
+ setup(async () => {
+ const promise = mockPromise();
+ const renderHandler = function () {
+ diffElement.removeEventListener('render', renderHandler);
+ cursor.reInitCursor();
+ promise.resolve();
+ };
+ diffElement.addEventListener('render', renderHandler);
+ diffElement.diff = {
+ ...diff,
+ content: [
+ {
+ ab: ['Lorem ipsum dolor sit amet, suspendisse inceptos vehicula, '],
+ },
+ {
+ b: [
+ 'Nullam neque, ligula ac, id blandit.',
+ 'Sagittis tincidunt torquent, tempor nunc amet.',
+ 'At rhoncus id.',
+ ],
+ move_details: {changed: false},
+ },
+ {
+ ab: ['Sem nascetur, erat ut, non in.'],
+ },
+ {
+ a: [
+ 'Nullam neque, ligula ac, id blandit.',
+ 'Sagittis tincidunt torquent, tempor nunc amet.',
+ 'At rhoncus id.',
+ ],
+ move_details: {changed: false},
+ },
+ {
+ ab: ['Arcu eget, rhoncus amet cursus, ipsum elementum.'],
+ },
+ ],
+ };
+ await promise;
+ });
+
+ test('renders moveControls with simple descriptions', () => {
+ const [movedIn, movedOut] = [
+ ...queryAll(diffElement, '.dueToMove .moveControls'),
+ ];
+ assert.equal(movedIn.textContent, 'Moved in');
+ assert.equal(movedOut.textContent, 'Moved out');
+ });
+ });
+
+ suite('moved chunks (moveDetails)', () => {
+ setup(async () => {
+ const promise = mockPromise();
+ const renderHandler = function () {
+ diffElement.removeEventListener('render', renderHandler);
+ cursor.reInitCursor();
+ promise.resolve();
+ };
+ diffElement.addEventListener('render', renderHandler);
+ diffElement.diff = {
+ ...diff,
+ content: [
+ {
+ ab: ['Lorem ipsum dolor sit amet, suspendisse inceptos vehicula, '],
+ },
+ {
+ b: [
+ 'Nullam neque, ligula ac, id blandit.',
+ 'Sagittis tincidunt torquent, tempor nunc amet.',
+ 'At rhoncus id.',
+ ],
+ move_details: {changed: false, range: {start: 4, end: 6}},
+ },
+ {
+ ab: ['Sem nascetur, erat ut, non in.'],
+ },
+ {
+ a: [
+ 'Nullam neque, ligula ac, id blandit.',
+ 'Sagittis tincidunt torquent, tempor nunc amet.',
+ 'At rhoncus id.',
+ ],
+ move_details: {changed: false, range: {start: 2, end: 4}},
+ },
+ {
+ ab: ['Arcu eget, rhoncus amet cursus, ipsum elementum.'],
+ },
+ ],
+ };
+ await promise;
+ });
+
+ test('renders moveControls with simple descriptions', () => {
+ const [movedIn, movedOut] = [
+ ...queryAll(diffElement, '.dueToMove .moveControls'),
+ ];
+ assert.equal(movedIn.textContent, 'Moved from lines 4 - 6');
+ assert.equal(movedOut.textContent, 'Moved to lines 2 - 4');
+ });
+
+ test('startLineAnchor of movedIn chunk fires events', async () => {
+ const [movedIn] = [...queryAll(diffElement, '.dueToMove .moveControls')];
+ const [startLineAnchor] = movedIn.querySelectorAll('a');
+
+ const promise = mockPromise();
+ const onMovedLinkClicked = (e: CustomEvent) => {
+ assert.deepEqual(e.detail, {lineNum: 4, side: Side.LEFT});
+ promise.resolve();
+ };
+ assert.equal(startLineAnchor.textContent, '4');
+ startLineAnchor.addEventListener(
+ 'moved-link-clicked',
+ onMovedLinkClicked
+ );
+ startLineAnchor.click();
+ await promise;
+ });
+
+ test('endLineAnchor of movedOut fires events', async () => {
+ const [, movedOut] = [
+ ...queryAll(diffElement, '.dueToMove .moveControls'),
+ ];
+ const [, endLineAnchor] = movedOut.querySelectorAll('a');
+
+ const promise = mockPromise();
+ const onMovedLinkClicked = (e: CustomEvent) => {
+ assert.deepEqual(e.detail, {lineNum: 4, side: Side.RIGHT});
+ promise.resolve();
+ };
+ assert.equal(endLineAnchor.textContent, '4');
+ endLineAnchor.addEventListener('moved-link-clicked', onMovedLinkClicked);
+ endLineAnchor.click();
+ await promise;
+ });
+ });
+
+ test('initialLineNumber not provided', async () => {
+ let scrollBehaviorDuringMove;
+ const moveToNumStub = sinon.stub(cursor, 'moveToLineNumber');
+ const moveToChunkStub = sinon
+ .stub(cursor, 'moveToFirstChunk')
+ .callsFake(() => {
+ scrollBehaviorDuringMove = cursor.cursorManager.scrollMode;
+ });
+
+ diffElement._diffChanged(createDiff());
+ await waitForEventOnce(diffElement, 'render');
+ cursor.reInitCursor();
+ assert.isFalse(moveToNumStub.called);
+ assert.isTrue(moveToChunkStub.called);
+ assert.equal(scrollBehaviorDuringMove, 'never');
+ assert.equal(cursor.cursorManager.scrollMode, 'keep-visible');
+ });
+
+ test('initialLineNumber provided', async () => {
+ let scrollBehaviorDuringMove;
+ const moveToNumStub = sinon
+ .stub(cursor, 'moveToLineNumber')
+ .callsFake(() => {
+ scrollBehaviorDuringMove = cursor.cursorManager.scrollMode;
+ });
+ const moveToChunkStub = sinon.stub(cursor, 'moveToFirstChunk');
+ cursor.initialLineNumber = 10;
+ cursor.side = Side.RIGHT;
+
+ diffElement._diffChanged(createDiff());
+ await waitForEventOnce(diffElement, 'render');
+ cursor.reInitCursor();
+ assert.isFalse(moveToChunkStub.called);
+ assert.isTrue(moveToNumStub.called);
+ assert.equal(moveToNumStub.lastCall.args[0], 10);
+ assert.equal(moveToNumStub.lastCall.args[1], Side.RIGHT);
+ assert.equal(scrollBehaviorDuringMove, 'keep-visible');
+ assert.equal(cursor.cursorManager.scrollMode, 'keep-visible');
+ });
+
+ test('getTargetDiffElement', () => {
+ cursor.initialLineNumber = 1;
+ assert.isTrue(!!cursor.diffRow);
+ assert.equal(cursor.getTargetDiffElement(), diffElement);
+ });
+
+ suite('createCommentInPlace', () => {
+ setup(() => {
+ diffElement.loggedIn = true;
+ });
+
+ test('adds new draft for selected line on the left', async () => {
+ cursor.moveToLineNumber(2, Side.LEFT);
+ const promise = mockPromise();
+ diffElement.addEventListener('create-comment', e => {
+ const {lineNum, range, side} = e.detail;
+ assert.equal(lineNum, 2);
+ assert.equal(range, undefined);
+ assert.equal(side, Side.LEFT);
+ promise.resolve();
+ });
+ cursor.createCommentInPlace();
+ await promise;
+ });
+
+ test('adds draft for selected line on the right', async () => {
+ cursor.moveToLineNumber(4, Side.RIGHT);
+ const promise = mockPromise();
+ diffElement.addEventListener('create-comment', e => {
+ const {lineNum, range, side} = e.detail;
+ assert.equal(lineNum, 4);
+ assert.equal(range, undefined);
+ assert.equal(side, Side.RIGHT);
+ promise.resolve();
+ });
+ cursor.createCommentInPlace();
+ await promise;
+ });
+
+ test('creates comment for range if selected', async () => {
+ const someRange = {
+ start_line: 2,
+ start_character: 3,
+ end_line: 6,
+ end_character: 1,
+ };
+ diffElement.highlights.selectedRange = {
+ side: Side.RIGHT,
+ range: someRange,
+ };
+ const promise = mockPromise();
+ diffElement.addEventListener('create-comment', e => {
+ const {lineNum, range, side} = e.detail;
+ assert.equal(lineNum, 6);
+ assert.equal(range, someRange);
+ assert.equal(side, Side.RIGHT);
+ promise.resolve();
+ });
+ cursor.createCommentInPlace();
+ await promise;
+ });
+
+ test('ignores call if nothing is selected', () => {
+ const createRangeCommentStub = sinon.stub(
+ diffElement,
+ 'createRangeComment'
+ );
+ const addDraftAtLineStub = sinon.stub(diffElement, 'addDraftAtLine');
+ cursor.diffRow = undefined;
+ cursor.createCommentInPlace();
+ assert.isFalse(createRangeCommentStub.called);
+ assert.isFalse(addDraftAtLineStub.called);
+ });
+ });
+
+ test('getAddress', () => {
+ // It should initialize to the first chunk: line 5 of the revision.
+ assert.deepEqual(cursor.getAddress(), {leftSide: false, number: 5});
+
+ // Revision line 4 is up.
+ cursor.moveUp();
+ assert.deepEqual(cursor.getAddress(), {leftSide: false, number: 4});
+
+ // Base line 4 is left.
+ cursor.moveLeft();
+ assert.deepEqual(cursor.getAddress(), {leftSide: true, number: 4});
+
+ // Moving to the next chunk takes it back to the start.
+ cursor.moveToNextChunk();
+ assert.deepEqual(cursor.getAddress(), {leftSide: false, number: 5});
+
+ // The following chunk is a removal starting on line 10 of the base.
+ cursor.moveToNextChunk();
+ assert.deepEqual(cursor.getAddress(), {leftSide: true, number: 10});
+
+ // Should be null if there is no selection.
+ cursor.cursorManager.unsetCursor();
+ assert.isNotOk(cursor.getAddress());
+ });
+
+ test('_findRowByNumberAndFile', () => {
+ // Get the first ab row after the first chunk.
+ const rows = [...queryAll<HTMLTableRowElement>(diffElement, 'tr')];
+ const row = rows[9];
+ assert.ok(row);
+
+ // It should be line 8 on the right, but line 5 on the left.
+ assert.equal(cursor._findRowByNumberAndFile(8, Side.RIGHT), row);
+ assert.equal(cursor._findRowByNumberAndFile(5, Side.LEFT), row);
+ });
+
+ test('expand context updates stops', async () => {
+ const spy = sinon.spy(cursor, '_updateStops');
+ const controls = queryAndAssert(diffElement, 'gr-context-controls');
+ const showContext = queryAndAssert<HTMLElement>(controls, '.showContext');
+ showContext.click();
+ await waitForEventOnce(diffElement, 'render');
+ assert.isTrue(spy.called);
+ });
+
+ test('updates stops when loading changes', () => {
+ const spy = sinon.spy(cursor, '_updateStops');
+ diffElement.dispatchEvent(new Event('loading-changed'));
+ assert.isTrue(spy.called);
+ });
+
+ suite('multi diff', () => {
+ let diffElements: GrDiff[];
+
+ setup(async () => {
+ diffElements = [
+ await fixture(html`<gr-diff></gr-diff>`),
+ await fixture(html`<gr-diff></gr-diff>`),
+ await fixture(html`<gr-diff></gr-diff>`),
+ ];
+ cursor = new GrDiffCursor();
+
+ // Register the diff with the cursor.
+ cursor.replaceDiffs(diffElements);
+
+ for (const el of diffElements) {
+ el.prefs = createDefaultDiffPrefs();
+ }
+ });
+
+ function getTargetDiffIndex() {
+ // Mocha has a bug where when `assert.equals` fails, it will try to
+ // JSON.stringify the operands, which fails when they are cyclic structures
+ // like GrDiffElement. The failure is difficult to attribute to a specific
+ // assertion because of the async nature assertion errors are handled and
+ // can cause the test simply timing out, causing a lot of debugging headache.
+ // Working with indices circumvents the problem.
+ const target = cursor.getTargetDiffElement();
+ assertIsDefined(target);
+ return diffElements.indexOf(target);
+ }
+
+ test('do not skip loading diffs', async () => {
+ diffElements[0].diff = createDiff();
+ diffElements[2].diff = createDiff();
+ await waitForEventOnce(diffElements[0], 'render');
+ await waitForEventOnce(diffElements[2], 'render');
+
+ const lastLine = diffElements[0].diff.meta_b?.lines;
+ assertIsDefined(lastLine);
+
+ // Goto second last line of the first diff
+ cursor.moveToLineNumber(lastLine - 1, Side.RIGHT);
+ assert.equal(
+ cursor.getTargetLineElement()!.textContent,
+ `${lastLine - 1}`
+ );
+
+ // Can move down until we reach the loading file
+ cursor.moveDown();
+ assert.equal(getTargetDiffIndex(), 0);
+ assert.equal(
+ cursor.getTargetLineElement()!.textContent,
+ lastLine.toString()
+ );
+
+ // Cannot move down while still loading the diff we would switch to
+ cursor.moveDown();
+ assert.equal(getTargetDiffIndex(), 0);
+ assert.equal(
+ cursor.getTargetLineElement()!.textContent,
+ lastLine.toString()
+ );
+
+ // Diff 1 finishing to load
+ diffElements[1].diff = createDiff();
+ await waitForEventOnce(diffElements[1], 'render');
+
+ // Now we can go down
+ cursor.moveDown();
+ assert.equal(getTargetDiffIndex(), 1);
+ assert.equal(cursor.getTargetLineElement()!.textContent, 'File');
+ });
+ });
+});
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group_test.js b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group_test.ts
similarity index 62%
rename from polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group_test.js
rename to polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group_test.ts
index 321086c..43a56d1 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group_test.js
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group_test.ts
@@ -1,32 +1,25 @@
/**
* @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.
+ * Copyright 2016 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
*/
-
-import '../../../test/common-test-setup-karma.js';
-import {GrDiffLine, GrDiffLineType, BLANK_LINE} from './gr-diff-line.js';
-import {GrDiffGroup, GrDiffGroupType, hideInContextControl} from './gr-diff-group.js';
+import '../../../test/common-test-setup-karma';
+import {GrDiffLine, GrDiffLineType, BLANK_LINE} from './gr-diff-line';
+import {
+ GrDiffGroup,
+ GrDiffGroupType,
+ hideInContextControl,
+} from './gr-diff-group';
suite('gr-diff-group tests', () => {
test('delta line pairs', () => {
const l1 = new GrDiffLine(GrDiffLineType.ADD, 0, 128);
const l2 = new GrDiffLine(GrDiffLineType.ADD, 0, 129);
const l3 = new GrDiffLine(GrDiffLineType.REMOVE, 64, 0);
- let group = new GrDiffGroup({type: GrDiffGroupType.DELTA, lines: [
- l1, l2, l3,
- ]});
+ let group = new GrDiffGroup({
+ type: GrDiffGroupType.DELTA,
+ lines: [l1, l2, l3],
+ });
assert.deepEqual(group.lines, [l1, l2, l3]);
assert.deepEqual(group.adds, [l1, l2]);
assert.deepEqual(group.removes, [l3]);
@@ -59,7 +52,9 @@
const l3 = new GrDiffLine(GrDiffLineType.BOTH, 66, 130);
const group = new GrDiffGroup({
- type: GrDiffGroupType.BOTH, lines: [l1, l2, l3]});
+ type: GrDiffGroupType.BOTH,
+ lines: [l1, l2, l3],
+ });
assert.deepEqual(group.lines, [l1, l2, l3]);
assert.deepEqual(group.adds, []);
@@ -83,34 +78,44 @@
const l2 = new GrDiffLine(GrDiffLineType.REMOVE);
const l3 = new GrDiffLine(GrDiffLineType.BOTH);
- assert.throws(() =>
- new GrDiffGroup({type: GrDiffGroupType.BOTH, lines: [l1, l2, l3]}));
+ assert.throws(
+ () => new GrDiffGroup({type: GrDiffGroupType.BOTH, lines: [l1, l2, l3]})
+ );
});
suite('hideInContextControl', () => {
- let groups;
+ let groups: GrDiffGroup[];
setup(() => {
groups = [
- new GrDiffGroup({type: GrDiffGroupType.BOTH, lines: [
- new GrDiffLine(GrDiffLineType.BOTH, 5, 7),
- new GrDiffLine(GrDiffLineType.BOTH, 6, 8),
- new GrDiffLine(GrDiffLineType.BOTH, 7, 9),
- ]}),
- new GrDiffGroup({type: GrDiffGroupType.DELTA, lines: [
- new GrDiffLine(GrDiffLineType.REMOVE, 8),
- new GrDiffLine(GrDiffLineType.ADD, 0, 10),
- new GrDiffLine(GrDiffLineType.REMOVE, 9),
- new GrDiffLine(GrDiffLineType.ADD, 0, 11),
- new GrDiffLine(GrDiffLineType.REMOVE, 10),
- new GrDiffLine(GrDiffLineType.ADD, 0, 12),
- new GrDiffLine(GrDiffLineType.REMOVE, 11),
- new GrDiffLine(GrDiffLineType.ADD, 0, 13),
- ]}),
- new GrDiffGroup({type: GrDiffGroupType.BOTH, lines: [
- new GrDiffLine(GrDiffLineType.BOTH, 12, 14),
- new GrDiffLine(GrDiffLineType.BOTH, 13, 15),
- new GrDiffLine(GrDiffLineType.BOTH, 14, 16),
- ]}),
+ new GrDiffGroup({
+ type: GrDiffGroupType.BOTH,
+ lines: [
+ new GrDiffLine(GrDiffLineType.BOTH, 5, 7),
+ new GrDiffLine(GrDiffLineType.BOTH, 6, 8),
+ new GrDiffLine(GrDiffLineType.BOTH, 7, 9),
+ ],
+ }),
+ new GrDiffGroup({
+ type: GrDiffGroupType.DELTA,
+ lines: [
+ new GrDiffLine(GrDiffLineType.REMOVE, 8),
+ new GrDiffLine(GrDiffLineType.ADD, 0, 10),
+ new GrDiffLine(GrDiffLineType.REMOVE, 9),
+ new GrDiffLine(GrDiffLineType.ADD, 0, 11),
+ new GrDiffLine(GrDiffLineType.REMOVE, 10),
+ new GrDiffLine(GrDiffLineType.ADD, 0, 12),
+ new GrDiffLine(GrDiffLineType.REMOVE, 11),
+ new GrDiffLine(GrDiffLineType.ADD, 0, 13),
+ ],
+ }),
+ new GrDiffGroup({
+ type: GrDiffGroupType.BOTH,
+ lines: [
+ new GrDiffLine(GrDiffLineType.BOTH, 12, 14),
+ new GrDiffLine(GrDiffLineType.BOTH, 13, 15),
+ new GrDiffLine(GrDiffLineType.BOTH, 14, 16),
+ ],
+ }),
];
});
@@ -140,21 +145,25 @@
assert.equal(collapsedGroups[2].contextGroups.length, 2);
assert.equal(
- collapsedGroups[2].contextGroups[0].type,
- GrDiffGroupType.DELTA);
+ collapsedGroups[2].contextGroups[0].type,
+ GrDiffGroupType.DELTA
+ );
assert.deepEqual(
- collapsedGroups[2].contextGroups[0].adds,
- groups[1].adds.slice(1));
+ collapsedGroups[2].contextGroups[0].adds,
+ groups[1].adds.slice(1)
+ );
assert.deepEqual(
- collapsedGroups[2].contextGroups[0].removes,
- groups[1].removes.slice(1));
+ collapsedGroups[2].contextGroups[0].removes,
+ groups[1].removes.slice(1)
+ );
assert.equal(
- collapsedGroups[2].contextGroups[1].type,
- GrDiffGroupType.BOTH);
- assert.deepEqual(
- collapsedGroups[2].contextGroups[1].lines,
- [groups[2].lines[0]]);
+ collapsedGroups[2].contextGroups[1].type,
+ GrDiffGroupType.BOTH
+ );
+ assert.deepEqual(collapsedGroups[2].contextGroups[1].lines, [
+ groups[2].lines[0],
+ ]);
assert.equal(collapsedGroups[3].type, GrDiffGroupType.BOTH);
assert.deepEqual(collapsedGroups[3].lines, groups[2].lines.slice(1));
@@ -166,19 +175,26 @@
type: GrDiffGroupType.BOTH,
skip: 60,
offsetLeft: 8,
- offsetRight: 10});
+ offsetRight: 10,
+ });
groups = [
- new GrDiffGroup({type: GrDiffGroupType.BOTH, lines: [
- new GrDiffLine(GrDiffLineType.BOTH, 5, 7),
- new GrDiffLine(GrDiffLineType.BOTH, 6, 8),
- new GrDiffLine(GrDiffLineType.BOTH, 7, 9),
- ]}),
+ new GrDiffGroup({
+ type: GrDiffGroupType.BOTH,
+ lines: [
+ new GrDiffLine(GrDiffLineType.BOTH, 5, 7),
+ new GrDiffLine(GrDiffLineType.BOTH, 6, 8),
+ new GrDiffLine(GrDiffLineType.BOTH, 7, 9),
+ ],
+ }),
skipGroup,
- new GrDiffGroup({type: GrDiffGroupType.BOTH, lines: [
- new GrDiffLine(GrDiffLineType.BOTH, 68, 70),
- new GrDiffLine(GrDiffLineType.BOTH, 69, 71),
- new GrDiffLine(GrDiffLineType.BOTH, 70, 72),
- ]}),
+ new GrDiffGroup({
+ type: GrDiffGroupType.BOTH,
+ lines: [
+ new GrDiffLine(GrDiffLineType.BOTH, 68, 70),
+ new GrDiffLine(GrDiffLineType.BOTH, 69, 71),
+ new GrDiffLine(GrDiffLineType.BOTH, 70, 72),
+ ],
+ }),
];
});
@@ -189,13 +205,11 @@
});
test('groups unchanged if the hidden range is empty', () => {
- assert.deepEqual(
- hideInContextControl(groups, 0, 0), groups);
+ assert.deepEqual(hideInContextControl(groups, 0, 0), groups);
});
test('groups unchanged if there is only 1 line to hide', () => {
- assert.deepEqual(
- hideInContextControl(groups, 3, 4), groups);
+ assert.deepEqual(hideInContextControl(groups, 3, 4), groups);
});
});
@@ -206,7 +220,7 @@
lines.push(new GrDiffLine(GrDiffLineType.ADD));
}
const group = new GrDiffGroup({type: GrDiffGroupType.DELTA, lines});
- assert.isTrue(group.isTotal(group));
+ assert.isTrue(group.isTotal());
});
test('is total for remove', () => {
@@ -215,12 +229,12 @@
lines.push(new GrDiffLine(GrDiffLineType.REMOVE));
}
const group = new GrDiffGroup({type: GrDiffGroupType.DELTA, lines});
- assert.isTrue(group.isTotal(group));
+ assert.isTrue(group.isTotal());
});
test('not total for empty', () => {
const group = new GrDiffGroup({type: GrDiffGroupType.BOTH});
- assert.isFalse(group.isTotal(group));
+ assert.isFalse(group.isTotal());
});
test('not total for non-delta', () => {
@@ -229,8 +243,7 @@
lines.push(new GrDiffLine(GrDiffLineType.BOTH));
}
const group = new GrDiffGroup({type: GrDiffGroupType.DELTA, lines});
- assert.isFalse(group.isTotal(group));
+ assert.isFalse(group.isTotal());
});
});
});
-
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
index e5f9de0..27952d3 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
@@ -43,11 +43,7 @@
import {getHiddenScroll} from '../../../scripts/hiddenscroll';
import {customElement, observe, property} from '@polymer/decorators';
import {BlameInfo, CommentRange, ImageInfo} from '../../../types/common';
-import {
- DiffInfo,
- DiffPreferencesInfo,
- DiffPreferencesInfoKey,
-} from '../../../types/diff';
+import {DiffInfo, DiffPreferencesInfo} from '../../../types/diff';
import {GrDiffHighlight} from '../gr-diff-highlight/gr-diff-highlight';
import {
GrDiffBuilderElement,
@@ -81,6 +77,7 @@
import {assertIsDefined} from '../../../utils/common-util';
import {debounce, DelayedTask} from '../../../utils/async-util';
import {GrDiffSelection} from '../gr-diff-selection/gr-diff-selection';
+import {deepEqual} from '../../../utils/deep-util';
const NO_NEWLINE_LEFT = 'No newline at end of left file.';
const NO_NEWLINE_RIGHT = 'No newline at end of right file.';
@@ -100,7 +97,6 @@
export interface GrDiff {
$: {
- diffBuilder: GrDiffBuilderElement;
diffTable: HTMLTableElement;
};
}
@@ -179,7 +175,7 @@
@property({type: Object})
highlightRange?: CommentRange;
- @property({type: Array})
+ @property({type: Array, observer: '_coverageRangesObserver'})
coverageRanges: CoverageRange[] = [];
@property({type: Boolean, observer: '_lineWrappingObserver'})
@@ -248,9 +244,6 @@
@property({type: Object, observer: '_blameChanged'})
blame: BlameInfo[] | null = null;
- @property({type: Number})
- parentIndex?: number;
-
@property({type: Boolean})
showNewlineWarningLeft = false;
@@ -292,11 +285,16 @@
@property({type: Boolean})
isAttached = false;
- private renderDiffTableTask?: DelayedTask;
+ // visible for testing
+ renderDiffTableTask?: DelayedTask;
private diffSelection = new GrDiffSelection();
- private highlights = new GrDiffHighlight();
+ // visible for testing
+ highlights = new GrDiffHighlight();
+
+ // visible for testing
+ diffBuilder = new GrDiffBuilderElement();
constructor() {
super();
@@ -321,11 +319,12 @@
this._unobserveNodes();
this.diffSelection.cleanup();
this.highlights.cleanup();
+ this.diffBuilder.cancel();
super.disconnectedCallback();
}
getLineNumEls(side: Side): HTMLElement[] {
- return this.$.diffBuilder.getLineNumEls(side);
+ return this.diffBuilder.getLineNumEls(side);
}
showNoChangeMessage(
@@ -426,19 +425,25 @@
cr.side === removedCommentRange.side &&
rangesEqual(cr.range, removedCommentRange.range)
);
- this.splice('_commentRanges', i, 1);
+ this._commentRanges.splice(i, 1);
}
- if (addedCommentRanges && addedCommentRanges.length) {
- this.push('_commentRanges', ...addedCommentRanges);
+ if (addedCommentRanges?.length) {
+ this._commentRanges.push(...addedCommentRanges);
}
if (this.highlightRange) {
- this.push('_commentRanges', {
+ this._commentRanges.push({
side: Side.RIGHT,
range: this.highlightRange,
rootId: '',
});
}
+
+ this.diffBuilder.updateCommentRanges(this._commentRanges);
+ }
+
+ _coverageRangesObserver() {
+ this.diffBuilder.updateCoverageRanges(this.coverageRanges);
}
/**
@@ -483,7 +488,7 @@
/** Cancel any remaining diff builder rendering work. */
cancel() {
- this.$.diffBuilder.cancel();
+ this.diffBuilder.cancel();
this.renderDiffTableTask?.cancel();
}
@@ -492,7 +497,7 @@
// Get rendered stops.
const stops: Array<HTMLElement | AbortStop> =
- this.$.diffBuilder.getLineNumberRows();
+ this.diffBuilder.getLineNumberRows();
// If we are still loading this diff, abort after the rendered stops to
// avoid skipping over to e.g. the next file.
@@ -512,7 +517,7 @@
_blameChanged(newValue?: BlameInfo[] | null) {
if (newValue === undefined) return;
- this.$.diffBuilder.setBlame(newValue);
+ this.diffBuilder.setBlame(newValue);
if (newValue) {
this.classList.add('showBlame');
} else {
@@ -534,7 +539,7 @@
return classes.join(' ');
}
- _handleTap(e: CustomEvent) {
+ _handleTap(e: Event) {
const el = (dom(e) as EventApi).localTarget as Element;
if (
@@ -603,7 +608,7 @@
_createCommentForSelection(side: Side, range: CommentRange) {
const lineNum = range.end_line;
- const lineEl = this.$.diffBuilder.getLineElByNumber(lineNum, side);
+ const lineEl = this.diffBuilder.getLineElByNumber(lineNum, side);
if (lineEl) {
this._createComment(lineEl, lineNum, side, range);
}
@@ -621,7 +626,7 @@
side?: Side,
range?: CommentRange
) {
- const contentEl = this.$.diffBuilder.getContentTdByLineEl(lineEl);
+ const contentEl = this.diffBuilder.getContentTdByLineEl(lineEl);
if (!contentEl) throw new Error('content el not found for line el');
side = side ?? this._getCommentSideByLineAndContent(lineEl, contentEl);
assertIsDefined(this.path, 'path');
@@ -663,28 +668,11 @@
}
_prefsObserver(newPrefs: DiffPreferencesInfo, oldPrefs: DiffPreferencesInfo) {
- if (!this._prefsEqual(newPrefs, oldPrefs)) {
+ if (!deepEqual(newPrefs, oldPrefs)) {
this._prefsChanged(newPrefs);
}
}
- _prefsEqual(prefs1: DiffPreferencesInfo, prefs2: DiffPreferencesInfo) {
- if (prefs1 === prefs2) {
- return true;
- }
- if (!prefs1 || !prefs2) {
- return false;
- }
- // Scan the preference objects one level deep to see if they differ.
- const keys1 = Object.keys(prefs1) as DiffPreferencesInfoKey[];
- const keys2 = Object.keys(prefs2) as DiffPreferencesInfoKey[];
- return (
- keys1.length === keys2.length &&
- keys1.every(key => prefs1[key] === prefs2[key]) &&
- keys2.every(key => prefs1[key] === prefs2[key])
- );
- }
-
_pathObserver() {
// Call _prefsChanged(), because line-limit style value depends on path.
this._prefsChanged(this.prefs);
@@ -699,7 +687,7 @@
if (!this.lineOfInterest) return;
const lineNum = this.lineOfInterest.lineNum;
if (typeof lineNum !== 'number') return;
- this.$.diffBuilder.unhideLine(lineNum, this.lineOfInterest.side);
+ this.diffBuilder.unhideLine(lineNum, this.lineOfInterest.side);
}
_cleanup() {
@@ -808,7 +796,7 @@
if (this.prefs) {
this._updatePreferenceStyles(this.prefs, renderPrefs);
}
- this.$.diffBuilder.updateRenderPrefs(renderPrefs);
+ this.diffBuilder.updateRenderPrefs(renderPrefs);
}
_diffChanged(newValue?: DiffInfo) {
@@ -820,7 +808,7 @@
}
if (this.diff) {
this.diffSelection.init(this.diff, this.$.diffTable);
- this.highlights.init(this.$.diffTable, this.$.diffBuilder);
+ this.highlights.init(this.$.diffTable, this.diffBuilder);
}
}
@@ -866,9 +854,24 @@
this._showWarning = false;
const keyLocations = this._computeKeyLocations();
- this.$.diffBuilder.prefs = this._getBypassPrefs(this.prefs);
- this.$.diffBuilder.renderPrefs = this.renderPrefs;
- this.$.diffBuilder.render(keyLocations);
+
+ // TODO: Setting tons of public properties like this is obviously a code
+ // smell. We are planning to introduce a diff model for managing all this
+ // data. Then diff builder will only need access to that model.
+ this.diffBuilder.prefs = this._getBypassPrefs(this.prefs);
+ this.diffBuilder.renderPrefs = this.renderPrefs;
+ this.diffBuilder.diff = this.diff;
+ this.diffBuilder.path = this.path;
+ this.diffBuilder.viewMode = this.viewMode;
+ this.diffBuilder.layers = this.layers ?? [];
+ this.diffBuilder.isImageDiff = this.isImageDiff;
+ this.diffBuilder.baseImage = this.baseImage ?? null;
+ this.diffBuilder.revisionImage = this.revisionImage ?? null;
+ this.diffBuilder.useNewImageDiffUi = this.useNewImageDiffUi;
+ this.diffBuilder.diffElement = this.$.diffTable;
+ this.diffBuilder.updateCommentRanges(this._commentRanges);
+ this.diffBuilder.updateCoverageRanges(this.coverageRanges);
+ this.diffBuilder.render(keyLocations);
}
_handleRenderContent() {
@@ -895,10 +898,7 @@
const commentSide = getSide(threadEl);
const range = getRange(threadEl);
if (!commentSide) continue;
- const lineEl = this.$.diffBuilder.getLineElByNumber(
- lineNum,
- commentSide
- );
+ const lineEl = this.diffBuilder.getLineElByNumber(lineNum, commentSide);
// When the line the comment refers to does not exist, log an error
// but don't crash. This can happen e.g. if the API does not fully
// validate e.g. (robot) comments
@@ -911,7 +911,7 @@
);
continue;
}
- const contentEl = this.$.diffBuilder.getContentTdByLineEl(lineEl);
+ const contentEl = this.diffBuilder.getContentTdByLineEl(lineEl);
if (!contentEl) continue;
if (lineNum === 'LOST' && !contentEl.hasChildNodes()) {
contentEl.appendChild(this._portedCommentsWithoutRangeMessage());
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_html.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_html.ts
index 6d36b89..40d4e7f 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_html.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_html.ts
@@ -698,36 +698,22 @@
class$="[[_computeContainerClass(loggedIn, viewMode, displayLine)]]"
on-click="_handleTap"
>
- <gr-diff-builder
- id="diffBuilder"
- comment-ranges="[[_commentRanges]]"
- coverage-ranges="[[coverageRanges]]"
- diff="[[diff]]"
- path="[[path]]"
- view-mode="[[viewMode]]"
- is-image-diff="[[isImageDiff]]"
- base-image="[[baseImage]]"
- layers="[[layers]]"
- revision-image="[[revisionImage]]"
- use-new-image-diff-ui="[[useNewImageDiffUi]]"
- >
- <table
- id="diffTable"
- class$="[[_diffTableClass]]"
- role="presentation"
- contenteditable$="[[isContentEditable]]"
- ></table>
+ <table
+ id="diffTable"
+ class$="[[_diffTableClass]]"
+ role="presentation"
+ contenteditable$="[[isContentEditable]]"
+ ></table>
- <template
- is="dom-if"
- if="[[showNoChangeMessage(_loading, prefs, _diffLength, diff)]]"
- >
- <div class="whitespace-change-only-message">
- This file only contains whitespace changes. Modify the whitespace
- setting to see the changes.
- </div>
- </template>
- </gr-diff-builder>
+ <template
+ is="dom-if"
+ if="[[showNoChangeMessage(_loading, prefs, _diffLength, diff)]]"
+ >
+ <div class="whitespace-change-only-message">
+ This file only contains whitespace changes. Modify the whitespace
+ setting to see the changes.
+ </div>
+ </template>
</div>
<div class$="[[_computeNewlineWarningClass(_newlineWarning, _loading)]]">
[[_newlineWarning]]
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.js b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.ts
similarity index 62%
rename from polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.js
rename to polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.ts
index c8b643d..183cdfb 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.js
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.ts
@@ -1,32 +1,37 @@
/**
* @license
- * Copyright (C) 2015 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.
+ * Copyright 2015 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
*/
-import '../../../test/common-test-setup-karma.js';
-import {createDiff} from '../../../test/test-data-generators.js';
-import './gr-diff.js';
-import {GrDiffBuilderImage} from '../gr-diff-builder/gr-diff-builder-image.js';
-import {getComputedStyleValue} from '../../../utils/dom-util.js';
-import {_setHiddenScroll} from '../../../scripts/hiddenscroll.js';
-import {runA11yAudit} from '../../../test/a11y-test-utils.js';
-import '@polymer/paper-button/paper-button.js';
-import {Side} from '../../../api/diff.js';
-import {mockPromise, stubRestApi} from '../../../test/test-utils.js';
-import {AbortStop} from '../../../api/core.js';
-import {afterNextRender} from '@polymer/polymer/lib/utils/render-status';
-import {waitForEventOnce} from '../../../utils/event-util.js';
+import '../../../test/common-test-setup-karma';
+import {createDiff} from '../../../test/test-data-generators';
+import './gr-diff';
+import {GrDiffBuilderImage} from '../gr-diff-builder/gr-diff-builder-image';
+import {getComputedStyleValue} from '../../../utils/dom-util';
+import {_setHiddenScroll} from '../../../scripts/hiddenscroll';
+import {runA11yAudit} from '../../../test/a11y-test-utils';
+import '@polymer/paper-button/paper-button';
+import {
+ DiffContent,
+ DiffInfo,
+ DiffPreferencesInfo,
+ DiffViewMode,
+ IgnoreWhitespaceType,
+ Side,
+} from '../../../api/diff';
+import {
+ mockPromise,
+ mouseDown,
+ query,
+ queryAll,
+ queryAndAssert,
+ stubRestApi,
+} from '../../../test/test-utils';
+import {AbortStop} from '../../../api/core';
+import {waitForEventOnce} from '../../../utils/event-util';
+import {GrDiff} from './gr-diff';
+import {ImageInfo} from '../../../types/common';
+import {GrRangedCommentHint} from '../gr-ranged-comment-hint/gr-ranged-comment-hint';
const basicFixture = fixtureFromElement('gr-diff');
@@ -37,42 +42,51 @@
});
suite('gr-diff tests', () => {
- let element;
+ let element: GrDiff;
- const MINIMAL_PREFS = {tab_size: 2, line_length: 80, font_size: 12};
+ const MINIMAL_PREFS: DiffPreferencesInfo = {
+ tab_size: 2,
+ line_length: 80,
+ font_size: 12,
+ context: 3,
+ ignore_whitespace: 'IGNORE_NONE',
+ };
- setup(() => {
-
- });
+ setup(() => {});
suite('selectionchange event handling', () => {
- const emulateSelection = function() {
+ let handleSelectionChangeStub: sinon.SinonSpy;
+
+ const emulateSelection = function () {
document.dispatchEvent(new CustomEvent('selectionchange'));
};
setup(() => {
element = basicFixture.instantiate();
- sinon.stub(element.highlights, 'handleSelectionChange');
+ handleSelectionChangeStub = sinon.spy(
+ element.highlights,
+ 'handleSelectionChange'
+ );
});
test('enabled if logged in', async () => {
element.loggedIn = true;
emulateSelection();
await flush();
- assert.isTrue(element.highlights.handleSelectionChange.called);
+ assert.isTrue(handleSelectionChangeStub.called);
});
test('ignored if logged out', async () => {
element.loggedIn = false;
emulateSelection();
await flush();
- assert.isFalse(element.highlights.handleSelectionChange.called);
+ assert.isFalse(handleSelectionChangeStub.called);
});
});
test('cancel', () => {
element = basicFixture.instantiate();
- const cancelStub = sinon.stub(element.$.diffBuilder, 'cancel');
+ const cancelStub = sinon.stub(element.diffBuilder, 'cancel');
element.cancel();
assert.isTrue(cancelStub.calledOnce);
});
@@ -98,10 +112,12 @@
});
test('line limit is based on line_length', () => {
- element.prefs = {...element.prefs, line_length: 100};
+ element.prefs = {...element.prefs!, line_length: 100};
flush();
- assert.equal(getComputedStyleValue('--line-limit-marker', element),
- '100ch');
+ assert.equal(
+ getComputedStyleValue('--line-limit-marker', element),
+ '100ch'
+ );
});
test('content-width should not be defined', () => {
@@ -123,32 +139,40 @@
});
test('max-width considers two content columns in side-by-side', () => {
- element.viewMode = 'SIDE_BY_SIDE';
+ element.viewMode = DiffViewMode.SIDE_BY_SIDE;
flush();
- assert.equal(getComputedStyleValue('--diff-max-width', element),
- 'calc(2 * 80ch + 2 * 48px + 0ch + 1px + 2px)');
+ assert.equal(
+ getComputedStyleValue('--diff-max-width', element),
+ 'calc(2 * 80ch + 2 * 48px + 0ch + 1px + 2px)'
+ );
});
test('max-width considers one content column in unified', () => {
- element.viewMode = 'UNIFIED_DIFF';
+ element.viewMode = DiffViewMode.UNIFIED;
flush();
- assert.equal(getComputedStyleValue('--diff-max-width', element),
- 'calc(1 * 80ch + 2 * 48px + 0ch + 1px + 2px)');
+ assert.equal(
+ getComputedStyleValue('--diff-max-width', element),
+ 'calc(1 * 80ch + 2 * 48px + 0ch + 1px + 2px)'
+ );
});
test('max-width considers font-size', () => {
- element.prefs = {...element.prefs, font_size: 13};
+ element.prefs = {...element.prefs!, font_size: 13};
flush();
// Each line number column: 4 * 13 = 52px
- assert.equal(getComputedStyleValue('--diff-max-width', element),
- 'calc(2 * 80ch + 2 * 52px + 0ch + 1px + 2px)');
+ assert.equal(
+ getComputedStyleValue('--diff-max-width', element),
+ 'calc(2 * 80ch + 2 * 52px + 0ch + 1px + 2px)'
+ );
});
test('sign cols are considered if show_sign_col is true', () => {
element.renderPrefs = {...element.renderPrefs, show_sign_col: true};
flush();
- assert.equal(getComputedStyleValue('--diff-max-width', element),
- 'calc(2 * 80ch + 2 * 48px + 2ch + 1px + 2px)');
+ assert.equal(
+ getComputedStyleValue('--diff-max-width', element),
+ 'calc(2 * 80ch + 2 * 48px + 2ch + 1px + 2px)'
+ );
});
});
@@ -168,39 +192,31 @@
});
test('view does not start with displayLine classList', () => {
- assert.isFalse(
- element.shadowRoot
- .querySelector('.diffContainer')
- .classList
- .contains('displayLine'));
+ const container = queryAndAssert(element, '.diffContainer');
+ assert.isFalse(container.classList.contains('displayLine'));
});
test('displayLine class added called when displayLine is true', () => {
const spy = sinon.spy(element, '_computeContainerClass');
element.displayLine = true;
+ const container = queryAndAssert(element, '.diffContainer');
assert.isTrue(spy.called);
- assert.isTrue(
- element.shadowRoot
- .querySelector('.diffContainer')
- .classList
- .contains('displayLine'));
+ assert.isTrue(container.classList.contains('displayLine'));
});
test('thread groups', () => {
const contentEl = document.createElement('div');
- element.changeNum = 123;
- element.patchRange = {basePatchNum: 1, patchNum: 2};
element.path = 'file.txt';
- element.$.diffBuilder.diff = createDiff();
- element.$.diffBuilder.prefs = {...MINIMAL_PREFS};
- element.$.diffBuilder._builder = element.$.diffBuilder._getDiffBuilder();
// No thread groups.
assert.equal(contentEl.querySelectorAll('.thread-group').length, 0);
// A thread group gets created.
- const threadGroupEl = element._getOrCreateThreadGroup(contentEl);
+ const threadGroupEl = element._getOrCreateThreadGroup(
+ contentEl,
+ Side.LEFT
+ );
assert.isOk(threadGroupEl);
// The new thread group can be fetched.
@@ -208,17 +224,19 @@
});
suite('image diffs', () => {
- let mockFile1;
- let mockFile2;
+ let mockFile1: ImageInfo;
+ let mockFile2: ImageInfo;
setup(() => {
mockFile1 = {
- body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' +
- 'wsAAAAAAAAAAAAAAAAA/w==',
+ body:
+ 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' +
+ 'wsAAAAAAAAAAAAAAAAA/w==',
type: 'image/bmp',
};
mockFile2 = {
- body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' +
- 'wsAAAAAAAAAAAAA/////w==',
+ body:
+ 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' +
+ 'wsAAAAAAAAAAAAA/////w==',
type: 'image/bmp',
};
@@ -235,7 +253,6 @@
show_whitespace_errors: true,
syntax_highlighting: true,
tab_size: 8,
- theme: 'DEFAULT',
};
});
@@ -244,8 +261,7 @@
element.revisionImage = mockFile2;
element.diff = {
meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
- meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg',
- lines: 560},
+ meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 560},
intraline_status: 'OK',
change_type: 'MODIFIED',
diff_header: [
@@ -262,42 +278,40 @@
// Recognizes that it should be an image diff.
assert.isTrue(element.isImageDiff);
- assert.instanceOf(
- element.$.diffBuilder._builder, GrDiffBuilderImage);
+ assert.instanceOf(element.diffBuilder.builder, GrDiffBuilderImage);
// Left image rendered with the parent commit's version of the file.
- const leftImage = element.$.diffTable.querySelector('td.left img');
- const leftLabel =
- element.$.diffTable.querySelector('td.left label');
- const leftLabelContent = leftLabel.querySelector('.label');
- const leftLabelName = leftLabel.querySelector('.name');
+ const diffTable = element.$.diffTable;
+ const leftImage = queryAndAssert(diffTable, 'td.left img');
+ const leftLabel = queryAndAssert(diffTable, 'td.left label');
+ const leftLabelContent = queryAndAssert(leftLabel, '.label');
+ const leftLabelName = query(leftLabel, '.name');
- const rightImage =
- element.$.diffTable.querySelector('td.right img');
- const rightLabel = element.$.diffTable.querySelector(
- 'td.right label');
- const rightLabelContent = rightLabel.querySelector('.label');
- const rightLabelName = rightLabel.querySelector('.name');
+ const rightImage = queryAndAssert(diffTable, 'td.right img');
+ const rightLabel = queryAndAssert(diffTable, 'td.right label');
+ const rightLabelContent = queryAndAssert(rightLabel, '.label');
+ const rightLabelName = query(rightLabel, '.name');
assert.isNotOk(rightLabelName);
assert.isNotOk(leftLabelName);
- assert.isOk(leftImage);
- assert.equal(leftImage.getAttribute('src'),
- 'data:image/bmp;base64,' + mockFile1.body);
- assert.equal(leftLabelContent.textContent, '1\u00d71 image/bmp');// \u00d7 - '×'
+ assert.equal(
+ leftImage.getAttribute('src'),
+ 'data:image/bmp;base64,' + mockFile1.body
+ );
+ assert.equal(leftLabelContent.textContent, '1\u00d71 image/bmp'); // \u00d7 - '×'
- assert.isOk(rightImage);
- assert.equal(rightImage.getAttribute('src'),
- 'data:image/bmp;base64,' + mockFile2.body);
- assert.equal(rightLabelContent.textContent, '1\u00d71 image/bmp');// \u00d7 - '×'
+ assert.equal(
+ rightImage.getAttribute('src'),
+ 'data:image/bmp;base64,' + mockFile2.body
+ );
+ assert.equal(rightLabelContent.textContent, '1\u00d71 image/bmp'); // \u00d7 - '×'
});
test('renders image diffs with a different file name', async () => {
- const mockDiff = {
+ const mockDiff: DiffInfo = {
meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
- meta_b: {name: 'carrot2.jpg', content_type: 'image/jpeg',
- lines: 560},
+ meta_b: {name: 'carrot2.jpg', content_type: 'image/jpeg', lines: 560},
intraline_status: 'OK',
change_type: 'MODIFIED',
diff_header: [
@@ -312,51 +326,51 @@
};
element.baseImage = mockFile1;
- element.baseImage._name = mockDiff.meta_a.name;
+ element.baseImage._name = mockDiff.meta_a!.name;
element.revisionImage = mockFile2;
- element.revisionImage._name = mockDiff.meta_b.name;
+ element.revisionImage._name = mockDiff.meta_b!.name;
element.diff = mockDiff;
await waitForEventOnce(element, 'render');
// Recognizes that it should be an image diff.
assert.isTrue(element.isImageDiff);
- assert.instanceOf(
- element.$.diffBuilder._builder, GrDiffBuilderImage);
+ assert.instanceOf(element.diffBuilder.builder, GrDiffBuilderImage);
// Left image rendered with the parent commit's version of the file.
- const leftImage = element.$.diffTable.querySelector('td.left img');
- const leftLabel =
- element.$.diffTable.querySelector('td.left label');
- const leftLabelContent = leftLabel.querySelector('.label');
- const leftLabelName = leftLabel.querySelector('.name');
+ const diffTable = element.$.diffTable;
+ const leftImage = queryAndAssert(diffTable, 'td.left img');
+ const leftLabel = queryAndAssert(diffTable, 'td.left label');
+ const leftLabelContent = queryAndAssert(leftLabel, '.label');
+ const leftLabelName = queryAndAssert(leftLabel, '.name');
- const rightImage =
- element.$.diffTable.querySelector('td.right img');
- const rightLabel = element.$.diffTable.querySelector(
- 'td.right label');
- const rightLabelContent = rightLabel.querySelector('.label');
- const rightLabelName = rightLabel.querySelector('.name');
+ const rightImage = queryAndAssert(diffTable, 'td.right img');
+ const rightLabel = queryAndAssert(diffTable, 'td.right label');
+ const rightLabelContent = queryAndAssert(rightLabel, '.label');
+ const rightLabelName = queryAndAssert(rightLabel, '.name');
assert.isOk(rightLabelName);
assert.isOk(leftLabelName);
- assert.equal(leftLabelName.textContent, mockDiff.meta_a.name);
- assert.equal(rightLabelName.textContent, mockDiff.meta_b.name);
+ assert.equal(leftLabelName.textContent, mockDiff.meta_a?.name);
+ assert.equal(rightLabelName.textContent, mockDiff.meta_b?.name);
assert.isOk(leftImage);
- assert.equal(leftImage.getAttribute('src'),
- 'data:image/bmp;base64,' + mockFile1.body);
- assert.equal(leftLabelContent.textContent, '1\u00d71 image/bmp');// \u00d7 - '×'
+ assert.equal(
+ leftImage.getAttribute('src'),
+ 'data:image/bmp;base64,' + mockFile1.body
+ );
+ assert.equal(leftLabelContent.textContent, '1\u00d71 image/bmp'); // \u00d7 - '×'
assert.isOk(rightImage);
- assert.equal(rightImage.getAttribute('src'),
- 'data:image/bmp;base64,' + mockFile2.body);
- assert.equal(rightLabelContent.textContent, '1\u00d71 image/bmp');// \u00d7 - '×'
+ assert.equal(
+ rightImage.getAttribute('src'),
+ 'data:image/bmp;base64,' + mockFile2.body
+ );
+ assert.equal(rightLabelContent.textContent, '1\u00d71 image/bmp'); // \u00d7 - '×'
});
test('renders added image', async () => {
- const mockDiff = {
- meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg',
- lines: 560},
+ const mockDiff: DiffInfo = {
+ meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 560},
intraline_status: 'OK',
change_type: 'ADDED',
diff_header: [
@@ -371,7 +385,9 @@
};
const promise = mockPromise();
- function rendered() { promise.resolve(); }
+ function rendered() {
+ promise.resolve();
+ }
element.addEventListener('render', rendered);
element.revisionImage = mockFile2;
@@ -380,20 +396,17 @@
element.removeEventListener('render', rendered);
// Recognizes that it should be an image diff.
assert.isTrue(element.isImageDiff);
- assert.instanceOf(
- element.$.diffBuilder._builder, GrDiffBuilderImage);
+ assert.instanceOf(element.diffBuilder.builder, GrDiffBuilderImage);
- const leftImage = element.$.diffTable.querySelector('td.left img');
- const rightImage = element.$.diffTable.querySelector('td.right img');
-
+ const diffTable = element.$.diffTable;
+ const leftImage = query(diffTable, 'td.left img');
assert.isNotOk(leftImage);
- assert.isOk(rightImage);
+ queryAndAssert(diffTable, 'td.right img');
});
test('renders removed image', async () => {
- const mockDiff = {
- meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg',
- lines: 560},
+ const mockDiff: DiffInfo = {
+ meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 560},
intraline_status: 'OK',
change_type: 'DELETED',
diff_header: [
@@ -407,7 +420,9 @@
binary: true,
};
const promise = mockPromise();
- function rendered() { promise.resolve(); }
+ function rendered() {
+ promise.resolve();
+ }
element.addEventListener('render', rendered);
element.baseImage = mockFile1;
@@ -416,20 +431,21 @@
element.removeEventListener('render', rendered);
// Recognizes that it should be an image diff.
assert.isTrue(element.isImageDiff);
- assert.instanceOf(
- element.$.diffBuilder._builder, GrDiffBuilderImage);
+ assert.instanceOf(element.diffBuilder.builder, GrDiffBuilderImage);
- const leftImage = element.$.diffTable.querySelector('td.left img');
- const rightImage = element.$.diffTable.querySelector('td.right img');
-
- assert.isOk(leftImage);
+ const diffTable = element.$.diffTable;
+ queryAndAssert(diffTable, 'td.left img');
+ const rightImage = query(diffTable, 'td.right img');
assert.isNotOk(rightImage);
});
test('does not render disallowed image type', async () => {
- const mockDiff = {
- meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg-evil',
- lines: 560},
+ const mockDiff: DiffInfo = {
+ meta_a: {
+ name: 'carrot.jpg',
+ content_type: 'image/jpeg-evil',
+ lines: 560,
+ },
intraline_status: 'OK',
change_type: 'DELETED',
diff_header: [
@@ -445,7 +461,9 @@
mockFile1.type = 'image/jpeg-evil';
const promise = mockPromise();
- function rendered() { promise.resolve(); }
+ function rendered() {
+ promise.resolve();
+ }
element.addEventListener('render', rendered);
element.baseImage = mockFile1;
@@ -454,9 +472,9 @@
element.removeEventListener('render', rendered);
// Recognizes that it should be an image diff.
assert.isTrue(element.isImageDiff);
- assert.instanceOf(
- element.$.diffBuilder._builder, GrDiffBuilderImage);
- const leftImage = element.$.diffTable.querySelector('td.left img');
+ assert.instanceOf(element.diffBuilder.builder, GrDiffBuilderImage);
+ const diffTable = element.$.diffTable;
+ const leftImage = query(diffTable, 'td.left img');
assert.isNotOk(leftImage);
});
});
@@ -513,7 +531,6 @@
show_tabs: true,
show_whitespace_errors: true,
syntax_highlighting: true,
- theme: 'DEFAULT',
ignore_whitespace: 'IGNORE_NONE',
};
@@ -548,20 +565,20 @@
const FILE_ROW = 1;
const actual = element.getCursorStops();
assert.equal(actual.length, ROWS + FILE_ROW + 1);
- assert.isTrue(actual[actual.length -1] instanceof AbortStop);
+ assert.isTrue(actual[actual.length - 1] instanceof AbortStop);
});
});
test('adds .hiddenscroll', () => {
_setHiddenScroll(true);
element.displayLine = true;
- assert.include(element.shadowRoot
- .querySelector('.diffContainer').className, 'hiddenscroll');
+ const container = queryAndAssert(element, '.diffContainer');
+ assert.include(container.className, 'hiddenscroll');
});
});
suite('logged in', () => {
- let fakeLineEl;
+ let fakeLineEl: HTMLElement;
setup(() => {
element = basicFixture.instantiate();
element.loggedIn = true;
@@ -571,15 +588,14 @@
classList: {
contains: sinon.stub().returns(true),
},
- };
+ } as unknown as HTMLElement;
});
test('addDraftAtLine', () => {
sinon.stub(element, '_selectLine');
- sinon.stub(element, '_createComment');
+ const createCommentStub = sinon.stub(element, '_createComment');
element.addDraftAtLine(fakeLineEl);
- assert.isTrue(element._createComment
- .calledWithExactly(fakeLineEl, 42));
+ assert.isTrue(createCommentStub.calledWithExactly(fakeLineEl, 42));
});
test('adds long range comment hint', async () => {
@@ -592,23 +608,29 @@
const threadEl = document.createElement('div');
threadEl.className = 'comment-thread';
threadEl.setAttribute('diff-side', 'right');
- threadEl.setAttribute('line-num', 1);
+ threadEl.setAttribute('line-num', '1');
threadEl.setAttribute('range', JSON.stringify(range));
threadEl.setAttribute('slot', 'right-1');
- const content = [{
- a: [],
- b: [],
- }, {
- ab: Array(13).fill('text'),
- }];
+ const content = [
+ {
+ a: [],
+ b: [],
+ },
+ {
+ ab: Array(13).fill('text'),
+ },
+ ];
setupSampleDiff({content});
- await new Promise(resolve => afterNextRender(element, resolve));
+ await waitForEventOnce(element, 'render');
element.appendChild(threadEl);
await flush();
- assert.deepEqual(
- element.querySelector('gr-ranged-comment-hint').range, range);
+ const hint = queryAndAssert<GrRangedCommentHint>(
+ element,
+ 'gr-ranged-comment-hint'
+ );
+ assert.deepEqual(hint.range, range);
});
test('no duplicate range hint for same thread', async () => {
@@ -621,19 +643,21 @@
const threadEl = document.createElement('div');
threadEl.className = 'comment-thread';
threadEl.setAttribute('diff-side', 'right');
- threadEl.setAttribute('line-num', 1);
+ threadEl.setAttribute('line-num', '1');
threadEl.setAttribute('range', JSON.stringify(range));
threadEl.setAttribute('slot', 'right-1');
const firstHint = document.createElement('gr-ranged-comment-hint');
firstHint.range = range;
- firstHint.setAttribute('threadElRootId', threadEl.rootId);
firstHint.setAttribute('slot', 'right-1');
- const content = [{
- a: [],
- b: [],
- }, {
- ab: Array(13).fill('text'),
- }];
+ const content = [
+ {
+ a: [],
+ b: [],
+ },
+ {
+ ab: Array(13).fill('text'),
+ },
+ ];
setupSampleDiff({content});
element.appendChild(firstHint);
@@ -644,86 +668,97 @@
await flush();
assert.equal(
- element.querySelectorAll('gr-ranged-comment-hint').length, 1);
+ element.querySelectorAll('gr-ranged-comment-hint').length,
+ 1
+ );
});
- test('removes long range comment hint when comment is discarded',
- async () => {
- const range = {
- start_line: 1,
- end_line: 7,
- start_character: 0,
- end_character: 0,
- };
- const threadEl = document.createElement('div');
- threadEl.className = 'comment-thread';
- threadEl.setAttribute('diff-side', 'right');
- threadEl.setAttribute('line-num', 1);
- threadEl.setAttribute('range', JSON.stringify(range));
- threadEl.setAttribute('slot', 'right-1');
- const content = [{
- a: [],
- b: [],
- }, {
- ab: Array(8).fill('text'),
- }];
- setupSampleDiff({content});
- element.appendChild(threadEl);
- await flush();
+ test('removes long range comment hint when comment is discarded', async () => {
+ const range = {
+ start_line: 1,
+ end_line: 7,
+ start_character: 0,
+ end_character: 0,
+ };
+ const threadEl = document.createElement('div');
+ threadEl.className = 'comment-thread';
+ threadEl.setAttribute('diff-side', 'right');
+ threadEl.setAttribute('line-num', '1');
+ threadEl.setAttribute('range', JSON.stringify(range));
+ threadEl.setAttribute('slot', 'right-1');
+ const content = [
+ {
+ a: [],
+ b: [],
+ },
+ {
+ ab: Array(8).fill('text'),
+ },
+ ];
+ setupSampleDiff({content});
+ element.appendChild(threadEl);
+ await flush();
- threadEl.remove();
- await flush();
+ threadEl.remove();
+ await flush();
- assert.isEmpty(element.querySelectorAll('gr-ranged-comment-hint'));
- });
+ assert.isEmpty(element.querySelectorAll('gr-ranged-comment-hint'));
+ });
suite('change in preferences', () => {
setup(() => {
element.diff = {
meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
- meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg',
- lines: 560},
+ meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 560},
diff_header: [],
intraline_status: 'OK',
change_type: 'MODIFIED',
content: [{skip: 66}],
};
- element.renderDiffTableTask.flush();
+ element.renderDiffTableTask?.flush();
});
test('change in preferences re-renders diff', () => {
- sinon.stub(element, '_renderDiffTable');
+ const stub = sinon.stub(element, '_renderDiffTable');
element.prefs = {
- ...MINIMAL_PREFS, time_format: 'HHMM_12'};
- element.renderDiffTableTask.flush();
- assert.isTrue(element._renderDiffTable.called);
+ ...MINIMAL_PREFS,
+ };
+ element.renderDiffTableTask?.flush();
+ assert.isTrue(stub.called);
});
test('adding/removing property in preferences re-renders diff', () => {
const stub = sinon.stub(element, '_renderDiffTable');
- const newPrefs1 = {...MINIMAL_PREFS,
- line_wrapping: true};
+ const newPrefs1: DiffPreferencesInfo = {
+ ...MINIMAL_PREFS,
+ line_wrapping: true,
+ };
element.prefs = newPrefs1;
- element.renderDiffTableTask.flush();
- assert.isTrue(element._renderDiffTable.called);
+ element.renderDiffTableTask?.flush();
+ assert.isTrue(stub.called);
stub.reset();
const newPrefs2 = {...newPrefs1};
delete newPrefs2.line_wrapping;
element.prefs = newPrefs2;
- element.renderDiffTableTask.flush();
- assert.isTrue(element._renderDiffTable.called);
+ element.renderDiffTableTask?.flush();
+ assert.isTrue(stub.called);
});
- test('change in preferences does not re-renders diff with ' +
- 'noRenderOnPrefsChange', () => {
- sinon.stub(element, '_renderDiffTable');
- element.noRenderOnPrefsChange = true;
- element.prefs = {
- ...MINIMAL_PREFS, time_format: 'HHMM_12'};
- element.renderDiffTableTask.flush();
- assert.isFalse(element._renderDiffTable.called);
- });
+ test(
+ 'change in preferences does not re-renders diff with ' +
+ 'noRenderOnPrefsChange',
+ () => {
+ const stub = sinon.stub(element, '_renderDiffTable');
+ element.noRenderOnPrefsChange = true;
+ element.prefs = {
+ ...MINIMAL_PREFS,
+ context: 12,
+ };
+ element.renderDiffTableTask?.flush();
+ assert.isFalse(stub.called);
+ }
+ );
});
});
@@ -732,8 +767,7 @@
element = basicFixture.instantiate();
element.diff = {
meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
- meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg',
- lines: 560},
+ meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 560},
diff_header: [],
intraline_status: 'OK',
change_type: 'MODIFIED',
@@ -755,11 +789,12 @@
assert.equal(element._diffHeaderItems.length, 1);
flush();
- assert.equal(element.$.diffHeader.textContent.trim(), 'test');
+ const header = queryAndAssert(element, '#diffHeader');
+ assert.equal(header.textContent?.trim(), 'test');
});
test('binary files', () => {
- element.diff.binary = true;
+ element.diff!.binary = true;
assert.equal(element._diffHeaderItems.length, 0);
element.push('diff.diff_header', 'diff --git a/test.jpg b/test.jpg');
assert.equal(element._diffHeaderItems.length, 0);
@@ -771,16 +806,17 @@
});
suite('safety and bypass', () => {
- let renderStub;
+ let renderStub: sinon.SinonStub;
setup(() => {
element = basicFixture.instantiate();
- renderStub = sinon.stub(element.$.diffBuilder, 'render').callsFake(
- () => {
- element.$.diffBuilder.dispatchEvent(
- new CustomEvent('render', {bubbles: true, composed: true}));
- return Promise.resolve({});
- });
+ renderStub = sinon.stub(element.diffBuilder, 'render').callsFake(() => {
+ const diffTable = element.$.diffTable;
+ diffTable.dispatchEvent(
+ new CustomEvent('render', {bubbles: true, composed: true})
+ );
+ return Promise.resolve({});
+ });
sinon.stub(element, 'getDiffLength').returns(10000);
element.diff = createDiff();
element.noRenderOnPrefsChange = true;
@@ -838,7 +874,7 @@
assert.equal(element.prefs.context, 3);
assert.equal(element._safetyBypass, -1);
- assert.equal(element.$.diffBuilder.prefs.context, -1);
+ assert.equal(element.diffBuilder.prefs.context, -1);
});
test('toggles collapse context from bypass', async () => {
@@ -851,7 +887,7 @@
assert.equal(element.prefs.context, 3);
assert.isNull(element._safetyBypass);
- assert.equal(element.$.diffBuilder.prefs.context, 3);
+ assert.equal(element.diffBuilder.prefs.context, 3);
});
test('toggles collapse context from pref using default', async () => {
@@ -863,7 +899,7 @@
assert.equal(element.prefs.context, -1);
assert.equal(element._safetyBypass, 10);
- assert.equal(element.$.diffBuilder.prefs.context, 10);
+ assert.equal(element.diffBuilder.prefs.context, 10);
});
});
@@ -874,7 +910,7 @@
test('unsetting', () => {
element.blame = [];
- const setBlameSpy = sinon.spy(element.$.diffBuilder, 'setBlame');
+ const setBlameSpy = sinon.spy(element.diffBuilder, 'setBlame');
element.classList.add('showBlame');
element.blame = null;
assert.isTrue(setBlameSpy.calledWithExactly(null));
@@ -882,7 +918,15 @@
});
test('setting', () => {
- element.blame = [{id: 'commit id', ranges: [{start: 1, end: 2}]}];
+ element.blame = [
+ {
+ author: 'test-author',
+ time: 12345,
+ commit_msg: '',
+ id: 'commit id',
+ ranges: [{start: 1, end: 2}],
+ },
+ ];
assert.isTrue(element.classList.contains('showBlame'));
});
});
@@ -891,8 +935,10 @@
const NO_NEWLINE_LEFT = 'No newline at end of left file.';
const NO_NEWLINE_RIGHT = 'No newline at end of right file.';
- const getWarning = element =>
- element.shadowRoot.querySelector('.newlineWarning').textContent;
+ const getWarning = (element: GrDiff) => {
+ const warningElement = queryAndAssert(element, '.newlineWarning');
+ return warningElement.textContent;
+ };
setup(() => {
element = basicFixture.instantiate();
@@ -904,8 +950,9 @@
element.showNewlineWarningLeft = true;
element.showNewlineWarningRight = true;
assert.include(
- getWarning(element),
- NO_NEWLINE_LEFT + ' \u2014 ' + NO_NEWLINE_RIGHT);// \u2014 - '—'
+ getWarning(element),
+ NO_NEWLINE_LEFT + ' \u2014 ' + NO_NEWLINE_RIGHT
+ ); // \u2014 - '—'
});
suite('showNewlineWarningLeft', () => {
@@ -918,11 +965,6 @@
element.showNewlineWarningLeft = false;
assert.notInclude(getWarning(element), NO_NEWLINE_LEFT);
});
-
- test('hide warning if undefined', () => {
- element.showNewlineWarningLeft = undefined;
- assert.notInclude(getWarning(element), NO_NEWLINE_LEFT);
- });
});
suite('showNewlineWarningRight', () => {
@@ -935,49 +977,25 @@
element.showNewlineWarningRight = false;
assert.notInclude(getWarning(element), NO_NEWLINE_RIGHT);
});
-
- test('hide warning if undefined', () => {
- element.showNewlineWarningRight = undefined;
- assert.notInclude(getWarning(element), NO_NEWLINE_RIGHT);
- });
});
test('_computeNewlineWarningClass', () => {
const hidden = 'newlineWarning hidden';
const shown = 'newlineWarning';
- assert.equal(element._computeNewlineWarningClass(null, true), hidden);
- assert.equal(element._computeNewlineWarningClass('foo', true), hidden);
- assert.equal(element._computeNewlineWarningClass(null, false), hidden);
- assert.equal(element._computeNewlineWarningClass('foo', false), shown);
- });
-
- test('_prefsEqual', () => {
- element = basicFixture.instantiate();
- assert.isTrue(element._prefsEqual(null, null));
- assert.isTrue(element._prefsEqual({}, {}));
- assert.isTrue(element._prefsEqual({x: 1}, {x: 1}));
- assert.isTrue(
- element._prefsEqual({x: 1, abc: 'def'}, {x: 1, abc: 'def'}));
- const somePref = {abc: 'def', p: true};
- assert.isTrue(element._prefsEqual(somePref, somePref));
-
- assert.isFalse(element._prefsEqual({}, null));
- assert.isFalse(element._prefsEqual(null, {}));
- assert.isFalse(element._prefsEqual({x: 1}, {x: 2}));
- assert.isFalse(element._prefsEqual({x: 1, y: 'abc'}, {x: 1, y: 'abcd'}));
- assert.isFalse(element._prefsEqual({x: 1, y: 'abc'}, {x: 1}));
- assert.isFalse(element._prefsEqual({x: 1}, {x: 1, y: 'abc'}));
+ assert.equal(element._computeNewlineWarningClass(false, true), hidden);
+ assert.equal(element._computeNewlineWarningClass(true, true), hidden);
+ assert.equal(element._computeNewlineWarningClass(false, false), hidden);
+ assert.equal(element._computeNewlineWarningClass(true, false), shown);
});
});
suite('key locations', () => {
- let renderStub;
+ let renderStub: sinon.SinonStub;
setup(() => {
element = basicFixture.instantiate();
- element.prefs = {};
- renderStub = sinon.stub(element.$.diffBuilder, 'render')
- .returns(new Promise(() => {}));
+ element.prefs = {...MINIMAL_PREFS};
+ renderStub = sinon.stub(element.diffBuilder, 'render');
});
test('lineOfInterest is a key location', () => {
@@ -994,7 +1012,7 @@
const threadEl = document.createElement('div');
threadEl.className = 'comment-thread';
threadEl.setAttribute('diff-side', 'right');
- threadEl.setAttribute('line-num', 3);
+ threadEl.setAttribute('line-num', '3');
element.appendChild(threadEl);
flush();
@@ -1021,7 +1039,11 @@
});
});
});
- const setupSampleDiff = function(params) {
+ const setupSampleDiff = function (params: {
+ content: DiffContent[];
+ ignore_whitespace?: IgnoreWhitespaceType;
+ binary?: boolean;
+ }) {
const {ignore_whitespace, content} = params;
// binary can't be undefined, use false if not set
const binary = params.binary || false;
@@ -1039,7 +1061,6 @@
show_whitespace_errors: true,
syntax_highlighting: true,
tab_size: 8,
- theme: 'DEFAULT',
};
element.diff = {
intraline_status: 'OK',
@@ -1059,21 +1080,24 @@
};
test('clear diff table content as soon as diff changes', () => {
- const content = [{
- a: ['all work and no play make andybons a dull boy'],
- }, {
- b: [
- 'Non eram nescius, Brute, cum, quae summis ingeniis ',
- ],
- }];
+ const content = [
+ {
+ a: ['all work and no play make andybons a dull boy'],
+ },
+ {
+ b: ['Non eram nescius, Brute, cum, quae summis ingeniis '],
+ },
+ ];
function assertDiffTableWithContent() {
- assert.isTrue(element.$.diffTable.innerText.includes(content[0].a));
+ const diffTable = element.$.diffTable;
+ assert.isTrue(diffTable.innerText.includes(content[0].a?.[0] ?? ''));
}
setupSampleDiff({content});
assertDiffTableWithContent();
- element.diff = {...element.diff};
+ element.diff = {...element.diff!};
// immediately cleaned up
- assert.equal(element.$.diffTable.innerHTML, '');
+ const diffTable = element.$.diffTable;
+ assert.equal(diffTable.innerHTML, '');
element._renderDiffTable();
flush();
// rendered again
@@ -1082,40 +1106,46 @@
suite('selection test', () => {
test('user-select set correctly on side-by-side view', () => {
- const 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',
- ],
- }];
+ const 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',
+ ],
+ },
+ ];
setupSampleDiff({content});
flush();
- const diffLine = element.shadowRoot.querySelectorAll('.contentText')[2];
+
+ const diffLine = queryAll<HTMLElement>(element, '.contentText')[2];
assert.equal(getComputedStyle(diffLine).userSelect, 'none');
- // click to mark it as selected
- MockInteractions.tap(diffLine);
+ mouseDown(diffLine);
assert.equal(getComputedStyle(diffLine).userSelect, 'text');
});
test('user-select set correctly on unified view', () => {
- const 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',
- ],
- }];
+ const 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',
+ ],
+ },
+ ];
setupSampleDiff({content});
- element.viewMode = 'UNIFIED_DIFF';
+ element.viewMode = DiffViewMode.UNIFIED;
flush();
- const diffLine = element.shadowRoot.querySelectorAll('.contentText')[2];
+ const diffLine = queryAll<HTMLElement>(element, '.contentText')[2];
assert.equal(getComputedStyle(diffLine).userSelect, 'none');
- MockInteractions.tap(diffLine);
+ mouseDown(diffLine);
assert.equal(getComputedStyle(diffLine).userSelect, 'text');
});
});
@@ -1123,71 +1153,87 @@
suite('whitespace changes only message', () => {
test('show the message if ignore_whitespace is criteria matches', () => {
setupSampleDiff({content: [{skip: 100}]});
- assert.isTrue(element.showNoChangeMessage(
+ assert.isTrue(
+ element.showNoChangeMessage(
/* loading= */ false,
element.prefs,
element._diffLength,
element.diff
- ));
+ )
+ );
});
test('do not show the message for binary files', () => {
setupSampleDiff({content: [{skip: 100}], binary: true});
- assert.isFalse(element.showNoChangeMessage(
+ assert.isFalse(
+ element.showNoChangeMessage(
/* loading= */ false,
element.prefs,
element._diffLength,
element.diff
- ));
+ )
+ );
});
test('do not show the message if still loading', () => {
setupSampleDiff({content: [{skip: 100}]});
- assert.isFalse(element.showNoChangeMessage(
+ assert.isFalse(
+ element.showNoChangeMessage(
/* loading= */ true,
element.prefs,
element._diffLength,
element.diff
- ));
+ )
+ );
});
test('do not show the message if contains valid changes', () => {
- const 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',
- ],
- }];
+ const 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',
+ ],
+ },
+ ];
setupSampleDiff({content});
assert.equal(element._diffLength, 3);
- assert.isFalse(element.showNoChangeMessage(
+ assert.isFalse(
+ element.showNoChangeMessage(
/* loading= */ false,
element.prefs,
element._diffLength,
element.diff
- ));
+ )
+ );
});
test('do not show message if ignore whitespace is disabled', () => {
- const 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',
- ],
- }];
+ const 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',
+ ],
+ },
+ ];
setupSampleDiff({ignore_whitespace: 'IGNORE_NONE', content});
- assert.isFalse(element.showNoChangeMessage(
+ assert.isFalse(
+ element.showNoChangeMessage(
/* loading= */ false,
element.prefs,
element._diffLength,
element.diff
- ));
+ )
+ );
});
});
@@ -1195,21 +1241,4 @@
const diff = createDiff();
assert.equal(element.getDiffLength(diff), 52);
});
-
- test('_prefsEqual', () => {
- element = basicFixture.instantiate();
- assert.isTrue(element._prefsEqual(null, null));
- assert.isTrue(element._prefsEqual({}, {}));
- assert.isTrue(element._prefsEqual({x: 1}, {x: 1}));
- assert.isTrue(element._prefsEqual({x: 1, abc: 'def'}, {x: 1, abc: 'def'}));
- const somePref = {abc: 'def', p: true};
- assert.isTrue(element._prefsEqual(somePref, somePref));
-
- assert.isFalse(element._prefsEqual({}, null));
- assert.isFalse(element._prefsEqual(null, {}));
- assert.isFalse(element._prefsEqual({x: 1}, {x: 2}));
- assert.isFalse(element._prefsEqual({x: 1, y: 'abc'}, {x: 1, y: 'abcd'}));
- assert.isFalse(element._prefsEqual({x: 1, y: 'abc'}, {x: 1}));
- assert.isFalse(element._prefsEqual({x: 1}, {x: 1, y: 'abc'}));
- });
});
diff --git a/polygerrit-ui/app/embed/gr-diff-app-context-init_test.js b/polygerrit-ui/app/embed/gr-diff-app-context-init_test.js
deleted file mode 100644
index bb46484..0000000
--- a/polygerrit-ui/app/embed/gr-diff-app-context-init_test.js
+++ /dev/null
@@ -1,32 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 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 {createDiffAppContext} from './gr-diff-app-context-init.js';
-
-suite('gr diff app context initializer tests', () => {
- test('all services initialized and are singletons', () => {
- const appContext = createDiffAppContext();
- Object.keys(appContext).forEach(serviceName => {
- const service = appContext[serviceName];
- assert.isNotNull(service);
- const service2 = appContext[serviceName];
- assert.strictEqual(service, service2);
- });
- });
-});
-
diff --git a/polygerrit-ui/app/embed/gr-diff-app-context-init_test.ts b/polygerrit-ui/app/embed/gr-diff-app-context-init_test.ts
new file mode 100644
index 0000000..84fd859
--- /dev/null
+++ b/polygerrit-ui/app/embed/gr-diff-app-context-init_test.ts
@@ -0,0 +1,22 @@
+/**
+ * @license
+ * Copyright 2020 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import {AppContext} from '../services/app-context';
+import '../test/common-test-setup-karma';
+import {createDiffAppContext} from './gr-diff-app-context-init';
+
+suite('gr diff app context initializer tests', () => {
+ test('all services initialized and are singletons', () => {
+ const appContext: AppContext = createDiffAppContext();
+ for (const serviceName of Object.keys(appContext) as Array<
+ keyof AppContext
+ >) {
+ const service = appContext[serviceName];
+ assert.isNotNull(service);
+ const service2 = appContext[serviceName];
+ assert.strictEqual(service, service2);
+ }
+ });
+});
diff --git a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
index 2b4fc60..3193833 100644
--- a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
+++ b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
@@ -2702,20 +2702,22 @@
return Promise.all([promiseA, promiseB]).then(results => {
// Sometimes the server doesn't send back the content type.
- const baseImage: Base64ImageFile | null = results[0]
- ? {
- ...results[0],
- _expectedType: diff.meta_a.content_type,
- _name: diff.meta_a.name,
- }
- : null;
- const revisionImage: Base64ImageFile | null = results[1]
- ? {
- ...results[1],
- _expectedType: diff.meta_b.content_type,
- _name: diff.meta_b.name,
- }
- : null;
+ const baseImage: Base64ImageFile | null =
+ results[0] && diff.meta_a
+ ? {
+ ...results[0],
+ _expectedType: diff.meta_a.content_type,
+ _name: diff.meta_a.name,
+ }
+ : null;
+ const revisionImage: Base64ImageFile | null =
+ results[1] && diff.meta_b
+ ? {
+ ...results[1],
+ _expectedType: diff.meta_b.content_type,
+ _name: diff.meta_b.name,
+ }
+ : null;
const imagesForDiff: ImagesForDiff = {baseImage, revisionImage};
return imagesForDiff;
});
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: {
diff --git a/polygerrit-ui/app/test/test-utils.ts b/polygerrit-ui/app/test/test-utils.ts
index 985bec1..ea7865e 100644
--- a/polygerrit-ui/app/test/test-utils.ts
+++ b/polygerrit-ui/app/test/test-utils.ts
@@ -27,7 +27,7 @@
import {ShortcutsService} from '../services/shortcuts/shortcuts-service';
import {queryAndAssert, query} from '../utils/common-util';
import {FlagsService} from '../services/flags/flags';
-import {Key, Modifier} from '../utils/dom-util';
+import {afterNextRender, Key, Modifier} from '../utils/dom-util';
import {Observable} from 'rxjs';
import {filter, take, timeout} from 'rxjs/operators';
import {HighlightService} from '../services/highlight/highlight-service';
@@ -224,6 +224,10 @@
return waitUntil(() => stub.called, `${name} was not called`);
}
+export async function nextRender() {
+ return new Promise(resolve => afterNextRender(resolve));
+}
+
/**
* Subscribes to the observable and resolves once it emits a matching value.
* Usage:
diff --git a/polygerrit-ui/app/types/diff.ts b/polygerrit-ui/app/types/diff.ts
index 562d47f..7ad656d 100644
--- a/polygerrit-ui/app/types/diff.ts
+++ b/polygerrit-ui/app/types/diff.ts
@@ -48,9 +48,9 @@
export interface DiffInfo extends DiffInfoApi {
/** Meta information about the file on side A as a DiffFileMetaInfo entity. */
- meta_a: DiffFileMetaInfo;
+ meta_a?: DiffFileMetaInfo;
/** Meta information about the file on side B as a DiffFileMetaInfo entity. */
- meta_b: DiffFileMetaInfo;
+ meta_b?: DiffFileMetaInfo;
/**
* Links to the file diff in external sites as a list of DiffWebLinkInfo
diff --git a/polygerrit-ui/app/utils/dom-util.ts b/polygerrit-ui/app/utils/dom-util.ts
index 16e0586..f2e0994 100644
--- a/polygerrit-ui/app/utils/dom-util.ts
+++ b/polygerrit-ui/app/utils/dom-util.ts
@@ -505,3 +505,14 @@
});
obs.observe(el);
}
+
+/**
+ * Mimics a Polymer utility. `requestAnimationFrame` is called before the next
+ * browser paint. An additional `setTimeout` ensures that the paint has
+ * actually happened.
+ */
+export function afterNextRender(callback: (value?: unknown) => void) {
+ requestAnimationFrame(() => {
+ setTimeout(callback);
+ });
+}
diff --git a/polygerrit-ui/app/utils/event-util.ts b/polygerrit-ui/app/utils/event-util.ts
index 418adbd..e624cef 100644
--- a/polygerrit-ui/app/utils/event-util.ts
+++ b/polygerrit-ui/app/utils/event-util.ts
@@ -32,7 +32,7 @@
);
}
-type HTMLElementEventDetailType<K extends keyof HTMLElementEventMap> =
+export type HTMLElementEventDetailType<K extends keyof HTMLElementEventMap> =
HTMLElementEventMap[K] extends CustomEvent<infer DT>
? unknown extends DT
? never