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