Merge "Migrate plugin endpoint components to lit" into stable-3.6
diff --git a/modules/jgit b/modules/jgit
index d73b7cd..e982de3 160000
--- a/modules/jgit
+++ b/modules/jgit
@@ -1 +1 @@
-Subproject commit d73b7cdeb4eaf32e3d41c105b974e620b33a168e
+Subproject commit e982de3fcb9f3a2e5ec6ceaae44cbb344962ea01
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.ts b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.ts
index 042ffc1..8d0eda8 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.ts
@@ -14,10 +14,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
import '../../../test/common-test-setup-karma';
import '../../shared/gr-date-formatter/gr-date-formatter';
-import {getMockDiffResponse} from '../../../test/mocks/diff-response';
import './gr-file-list';
import {createCommentApiMockWithTemplateElement} from '../../../test/mocks/comment-api';
import {FilesExpandedState} from '../gr-file-list-constants';
@@ -47,6 +45,7 @@
import {
createChangeComments,
createCommit,
+ createDiff,
createParsedChange,
createRevision,
} from '../../../test/test-data-generators';
@@ -1754,7 +1753,7 @@
syntax_highlighting: true,
ignore_whitespace: 'IGNORE_NONE',
};
- diff.diff = getMockDiffResponse();
+ diff.diff = createDiff();
await listenOnce(diff, 'render');
}
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.ts b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.ts
index 6bf0780..205524f 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.ts
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.ts
@@ -19,89 +19,241 @@
import '../../shared/gr-date-formatter/gr-date-formatter';
import '../../../styles/gr-form-styles';
import '../../../styles/shared-styles';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-account-info_html';
-import {customElement, property, observe} from '@polymer/decorators';
import {AccountDetailInfo, ServerInfo} from '../../../types/common';
import {EditableAccountField} from '../../../constants/constants';
import {getAppContext} from '../../../services/app-context';
-import {fireEvent} from '../../../utils/event-util';
+import {fire, fireEvent} from '../../../utils/event-util';
+import {LitElement, css, html, nothing, PropertyValues} from 'lit';
+import {customElement, property, state} from 'lit/decorators';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {formStyles} from '../../../styles/gr-form-styles';
+import {when} from 'lit/directives/when';
+import {BindValueChangeEvent, ValueChangedEvent} from '../../../types/events';
@customElement('gr-account-info')
-export class GrAccountInfo extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
-
+export class GrAccountInfo extends LitElement {
/**
* Fired when account details are changed.
*
* @event account-detail-update
*/
- @property({
- type: Boolean,
- notify: true,
- computed: '_computeUsernameMutable(_serverConfig, _account.username)',
- })
- usernameMutable?: boolean;
+ // private but used in test
+ @state() usernameMutable?: boolean;
- @property({
- type: Boolean,
- notify: true,
- computed: '_computeNameMutable(_serverConfig)',
- })
- nameMutable?: boolean;
+ // private but used in test
+ @state() nameMutable?: boolean;
- @property({
- type: Boolean,
- notify: true,
- computed:
- '_computeHasUnsavedChanges(_hasNameChange, ' +
- '_hasUsernameChange, _hasStatusChange, _hasDisplayNameChange)',
- })
- hasUnsavedChanges?: boolean;
+ @property({type: Boolean}) hasUnsavedChanges = false;
- @property({type: Boolean})
- _hasNameChange?: boolean;
+ // private but used in test
+ @state() hasNameChange = false;
- @property({type: Boolean})
- _hasUsernameChange?: boolean;
+ // private but used in test
+ @state() hasUsernameChange = false;
- @property({type: Boolean})
- _hasDisplayNameChange?: boolean;
+ // private but used in test
+ @state() hasDisplayNameChange = false;
- @property({type: Boolean})
- _hasStatusChange?: boolean;
+ // private but used in test
+ @state() hasStatusChange = false;
- @property({type: Boolean})
- _loading = false;
+ // private but used in test
+ @state() loading = false;
- @property({type: Boolean})
- _saving = false;
+ @state() private saving = false;
- @property({type: Object})
- _account?: AccountDetailInfo;
+ // private but used in test
+ @state() account?: AccountDetailInfo;
- @property({type: Object})
- _serverConfig?: ServerInfo;
+ // private but used in test
+ @state() serverConfig?: ServerInfo;
- @property({type: String, observer: '_usernameChanged'})
- _username?: string;
+ // private but used in test
+ @state() username?: string;
- @property({type: String})
- _avatarChangeUrl = '';
+ @state() private avatarChangeUrl = '';
private readonly restApiService = getAppContext().restApiService;
+ static override styles = [
+ sharedStyles,
+ formStyles,
+ css`
+ gr-avatar {
+ height: 120px;
+ width: 120px;
+ margin-right: var(--spacing-xs);
+ vertical-align: -0.25em;
+ }
+ div section.hide {
+ display: none;
+ }
+ `,
+ ];
+
+ override render() {
+ if (!this.account || this.loading) return nothing;
+ return html`<div class="gr-form-styles">
+ <section>
+ <span class="title"></span>
+ <span class="value">
+ <gr-avatar .account=${this.account} imageSize="120"></gr-avatar>
+ </span>
+ </section>
+ ${when(
+ this.avatarChangeUrl,
+ () => html` <section>
+ <span class="title"></span>
+ <span class="value">
+ <a href=${this.avatarChangeUrl}> Change avatar </a>
+ </span>
+ </section>`
+ )}
+ <section>
+ <span class="title">ID</span>
+ <span class="value">${this.account._account_id}</span>
+ </section>
+ <section>
+ <span class="title">Email</span>
+ <span class="value">${this.account.email}</span>
+ </section>
+ <section>
+ <span class="title">Registered</span>
+ <span class="value">
+ <gr-date-formatter
+ withTooltip
+ .dateStr=${this.account.registered_on}
+ ></gr-date-formatter>
+ </span>
+ </section>
+ <section id="usernameSection">
+ <span class="title">Username</span>
+ ${when(
+ this.usernameMutable,
+ () => html`<span class="value">
+ <iron-input
+ @keydown=${this.handleKeydown}
+ .bindValue=${this.username}
+ @bind-value-changed=${(e: BindValueChangeEvent) => {
+ if (!this.username || this.username === e.detail.value) return;
+ this.username = e.detail.value;
+ this.hasUsernameChange = true;
+ }}
+ id="usernameIronInput"
+ >
+ <input
+ id="usernameInput"
+ ?disabled=${this.saving}
+ @keydown=${this.handleKeydown}
+ />
+ </iron-input>
+ </span>`,
+ () => html`<span class="value">${this.username}</span>`
+ )}
+ </section>
+ <section id="nameSection">
+ <label class="title" for="nameInput">Full name</label>
+ ${when(
+ this.nameMutable,
+ () => html`<span class="value">
+ <iron-input
+ @keydown=${this.handleKeydown}
+ .bindValue=${this.account?.name}
+ @bind-value-changed=${(e: BindValueChangeEvent) => {
+ const oldAccount = this.account;
+ if (!oldAccount || oldAccount.name === e.detail.value) return;
+ this.account = {...oldAccount, name: e.detail.value};
+ this.hasNameChange = true;
+ }}
+ id="nameIronInput"
+ >
+ <input
+ id="nameInput"
+ ?disabled=${this.saving}
+ @keydown=${this.handleKeydown}
+ />
+ </iron-input>
+ </span>`,
+ () => html` <span class="value">${this.account?.name}</span>`
+ )}
+ </section>
+ <section>
+ <label class="title" for="displayNameInput">Display name</label>
+ <span class="value">
+ <iron-input
+ @keydown=${this.handleKeydown}
+ .bindValue=${this.account.display_name}
+ @bind-value-changed=${(e: BindValueChangeEvent) => {
+ const oldAccount = this.account;
+ if (!oldAccount || oldAccount.display_name === e.detail.value) {
+ return;
+ }
+ this.account = {...oldAccount, display_name: e.detail.value};
+ this.hasDisplayNameChange = true;
+ }}
+ >
+ <input
+ id="displayNameInput"
+ ?disabled=${this.saving}
+ @keydown=${this.handleKeydown}
+ />
+ </iron-input>
+ </span>
+ </section>
+ <section>
+ <label class="title" for="statusInput">About me (e.g. employer)</label>
+ <span class="value">
+ <iron-input
+ id="statusIronInput"
+ @keydown=${this.handleKeydown}
+ .bindValue=${this.account?.status}
+ @bind-value-changed=${(e: BindValueChangeEvent) => {
+ const oldAccount = this.account;
+ if (!oldAccount || oldAccount.status === e.detail.value) return;
+ this.account = {...oldAccount, status: e.detail.value};
+ this.hasStatusChange = true;
+ }}
+ >
+ <input
+ id="statusInput"
+ ?disabled=${this.saving}
+ @keydown=${this.handleKeydown}
+ />
+ </iron-input>
+ </span>
+ </section>
+ </div>`;
+ }
+
+ override willUpdate(changedProperties: PropertyValues) {
+ if (changedProperties.has('serverConfig')) {
+ this.usernameMutable = this.computeUsernameMutable();
+ this.nameMutable = this.computeNameMutable();
+ }
+ if (
+ changedProperties.has('hasNameChange') ||
+ changedProperties.has('hasUsernameChange') ||
+ changedProperties.has('hasStatusChange') ||
+ changedProperties.has('hasDisplayNameChange')
+ ) {
+ this.hasUnsavedChanges = this.computeHasUnsavedChanges();
+ }
+ if (changedProperties.has('hasUnsavedChanges')) {
+ fire(this, 'unsaved-changes-changed', {
+ value: this.hasUnsavedChanges,
+ });
+ }
+ }
+
loadData() {
const promises = [];
- this._loading = true;
+ this.loading = true;
promises.push(
this.restApiService.getConfig().then(config => {
- this._serverConfig = config;
+ this.serverConfig = config;
})
);
@@ -110,26 +262,26 @@
promises.push(
this.restApiService.getAccount().then(account => {
if (!account) return;
- this._hasNameChange = false;
- this._hasUsernameChange = false;
- this._hasDisplayNameChange = false;
- this._hasStatusChange = false;
+ this.hasNameChange = false;
+ this.hasUsernameChange = false;
+ this.hasDisplayNameChange = false;
+ this.hasStatusChange = false;
// Provide predefined value for username to trigger computation of
// username mutability.
account.username = account.username || '';
- this._account = account;
- this._username = account.username;
+ this.account = account;
+ this.username = account.username;
})
);
promises.push(
this.restApiService.getAvatarChangeUrl().then(url => {
- this._avatarChangeUrl = url || '';
+ this.avatarChangeUrl = url || '';
})
);
return Promise.all(promises).then(() => {
- this._loading = false;
+ this.loading = false;
});
}
@@ -138,132 +290,89 @@
return Promise.resolve();
}
- this._saving = true;
+ this.saving = true;
// Set only the fields that have changed.
// Must be done in sequence to avoid race conditions (@see Issue 5721)
- return this._maybeSetName()
- .then(() => this._maybeSetUsername())
- .then(() => this._maybeSetDisplayName())
- .then(() => this._maybeSetStatus())
+ return this.maybeSetName()
+ .then(() => this.maybeSetUsername())
+ .then(() => this.maybeSetDisplayName())
+ .then(() => this.maybeSetStatus())
.then(() => {
- this._hasNameChange = false;
- this._hasDisplayNameChange = false;
- this._hasStatusChange = false;
- this._saving = false;
+ this.hasNameChange = false;
+ this.hasDisplayNameChange = false;
+ this.hasStatusChange = false;
+ this.saving = false;
fireEvent(this, 'account-detail-update');
});
}
- _maybeSetName() {
+ private maybeSetName() {
// Note that we are intentionally not acting on this._account.name being the
// empty string (which is falsy).
- return this._hasNameChange && this.nameMutable && this._account?.name
- ? this.restApiService.setAccountName(this._account.name)
+ return this.hasNameChange && this.nameMutable && this.account?.name
+ ? this.restApiService.setAccountName(this.account.name)
: Promise.resolve();
}
- _maybeSetUsername() {
+ private maybeSetUsername() {
// Note that we are intentionally not acting on this._username being the
// empty string (which is falsy).
- return this._hasUsernameChange && this.usernameMutable && this._username
- ? this.restApiService.setAccountUsername(this._username)
+ return this.hasUsernameChange && this.usernameMutable && this.username
+ ? this.restApiService.setAccountUsername(this.username)
: Promise.resolve();
}
- _maybeSetDisplayName() {
- return this._hasDisplayNameChange &&
- this._account?.display_name !== undefined
- ? this.restApiService.setAccountDisplayName(this._account.display_name)
+ private maybeSetDisplayName() {
+ return this.hasDisplayNameChange && this.account?.display_name !== undefined
+ ? this.restApiService.setAccountDisplayName(this.account.display_name)
: Promise.resolve();
}
- _maybeSetStatus() {
- return this._hasStatusChange && this._account?.status !== undefined
- ? this.restApiService.setAccountStatus(this._account.status)
+ private maybeSetStatus() {
+ return this.hasStatusChange && this.account?.status !== undefined
+ ? this.restApiService.setAccountStatus(this.account.status)
: Promise.resolve();
}
- _computeHasUnsavedChanges(
- nameChanged: boolean,
- usernameChanged: boolean,
- statusChanged: boolean,
- displayNameChanged: boolean
- ) {
+ private computeHasUnsavedChanges() {
return (
- nameChanged || usernameChanged || statusChanged || displayNameChanged
+ this.hasNameChange ||
+ this.hasUsernameChange ||
+ this.hasStatusChange ||
+ this.hasDisplayNameChange
);
}
- _computeUsernameMutable(config: ServerInfo, username?: string) {
- // Polymer 2: check for undefined
- if ([config, username].includes(undefined)) {
- return undefined;
- }
-
+ private computeUsernameMutable() {
+ if (!this.serverConfig) return false;
// Username may not be changed once it is set.
return (
- config.auth.editable_account_fields.includes(
+ this.serverConfig.auth.editable_account_fields.includes(
EditableAccountField.USER_NAME
- ) && !username
+ ) && !this.account?.username
);
}
- _computeNameMutable(config: ServerInfo) {
- return config.auth.editable_account_fields.includes(
+ private computeNameMutable() {
+ if (!this.serverConfig) return false;
+ return this.serverConfig.auth.editable_account_fields.includes(
EditableAccountField.FULL_NAME
);
}
- @observe('_account.status')
- _statusChanged() {
- if (this._loading) {
- return;
- }
- this._hasStatusChange = true;
- }
-
- @observe('_account.display_name')
- _displayNameChanged() {
- if (this._loading) {
- return;
- }
- this._hasDisplayNameChange = true;
- }
-
- _usernameChanged() {
- if (this._loading || !this._account) {
- return;
- }
- this._hasUsernameChange =
- (this._account.username || '') !== (this._username || '');
- }
-
- @observe('_account.name')
- _nameChanged() {
- if (this._loading) {
- return;
- }
- this._hasNameChange = true;
- }
-
- _handleKeydown(e: KeyboardEvent) {
+ private handleKeydown(e: KeyboardEvent) {
if (e.keyCode === 13) {
// Enter
e.stopPropagation();
this.save();
}
}
-
- _hideAvatarChangeUrl(avatarChangeUrl: string) {
- if (!avatarChangeUrl) {
- return 'hide';
- }
-
- return '';
- }
}
declare global {
+ interface HTMLElementEventMap {
+ 'unsaved-changes-changed': ValueChangedEvent<boolean>;
+ }
interface HTMLElementTagNameMap {
'gr-account-info': GrAccountInfo;
}
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_html.ts b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_html.ts
deleted file mode 100644
index a6ea1f6..0000000
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_html.ts
+++ /dev/null
@@ -1,129 +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`
- <style include="shared-styles">
- gr-avatar {
- height: 120px;
- width: 120px;
- margin-right: var(--spacing-xs);
- vertical-align: -0.25em;
- }
- div section.hide {
- display: none;
- }
- </style>
- <style include="gr-form-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <div class="gr-form-styles">
- <section>
- <span class="title"></span>
- <span class="value">
- <gr-avatar account="[[_account]]" imageSize="120"></gr-avatar>
- </span>
- </section>
- <section class$="[[_hideAvatarChangeUrl(_avatarChangeUrl)]]">
- <span class="title"></span>
- <span class="value">
- <a href$="[[_avatarChangeUrl]]"> Change avatar </a>
- </span>
- </section>
- <section>
- <span class="title">ID</span>
- <span class="value">[[_account._account_id]]</span>
- </section>
- <section>
- <span class="title">Email</span>
- <span class="value">[[_account.email]]</span>
- </section>
- <section>
- <span class="title">Registered</span>
- <span class="value">
- <gr-date-formatter
- withTooltip
- date-str="[[_account.registered_on]]"
- ></gr-date-formatter>
- </span>
- </section>
- <section id="usernameSection">
- <span class="title">Username</span>
- <span hidden$="[[usernameMutable]]" class="value">[[_username]]</span>
- <span hidden$="[[!usernameMutable]]" class="value">
- <iron-input
- on-keydown="_handleKeydown"
- bind-value="{{_username}}"
- id="usernameIronInput"
- >
- <input
- id="usernameInput"
- disabled="[[_saving]]"
- on-keydown="_handleKeydown"
- />
- </iron-input>
- </span>
- </section>
- <section id="nameSection">
- <label class="title" for="nameInput">Full name</label>
- <span hidden$="[[nameMutable]]" class="value">[[_account.name]]</span>
- <span hidden$="[[!nameMutable]]" class="value">
- <iron-input
- on-keydown="_handleKeydown"
- bind-value="{{_account.name}}"
- id="nameIronInput"
- >
- <input
- id="nameInput"
- disabled="[[_saving]]"
- on-keydown="_handleKeydown"
- />
- </iron-input>
- </span>
- </section>
- <section>
- <label class="title" for="displayNameInput">Display name</label>
- <span class="value">
- <iron-input
- on-keydown="_handleKeydown"
- bind-value="{{_account.display_name}}"
- >
- <input
- id="displayNameInput"
- disabled="[[_saving]]"
- on-keydown="_handleKeydown"
- />
- </iron-input>
- </span>
- </section>
- <section>
- <label class="title" for="statusInput">About me (e.g. employer)</label>
- <span class="value">
- <iron-input
- on-keydown="_handleKeydown"
- bind-value="{{_account.status}}"
- >
- <input
- id="statusInput"
- disabled="[[_saving]]"
- on-keydown="_handleKeydown"
- />
- </iron-input>
- </span>
- </section>
- </div>
-`;
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.ts b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.ts
index a12d289..826046d 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.ts
@@ -17,18 +17,20 @@
import '../../../test/common-test-setup-karma';
import './gr-account-info';
-import {SinonSpyMember, stubRestApi} from '../../../test/test-utils';
+import {query, queryAll, stubRestApi} from '../../../test/test-utils';
import {GrAccountInfo} from './gr-account-info';
import {AccountDetailInfo, ServerInfo} from '../../../types/common';
import {
createAccountDetailWithId,
createAccountWithIdNameAndEmail,
+ createAuth,
createPreferences,
createServerInfo,
} from '../../../test/test-data-generators';
import {IronInputElement} from '@polymer/iron-input';
import {SinonStubbedMember} from 'sinon';
import {RestApiService} from '../../../services/gr-rest-api/gr-rest-api';
+import {EditableAccountField} from '../../../api/rest-api';
const basicFixture = fixtureFromElement('gr-account-info');
@@ -38,13 +40,13 @@
let config: ServerInfo;
function queryIronInput(selector: string): IronInputElement {
- const input = element.root?.querySelector<IronInputElement>(selector);
+ const input = query<IronInputElement>(element, selector);
if (!input) assert.fail(`<iron-input> with id ${selector} not found.`);
return input;
}
function valueOf(title: string): Element {
- const sections = element.root?.querySelectorAll('section') ?? [];
+ const sections = queryAll<HTMLElement>(element, 'section') ?? [];
let titleEl;
for (let i = 0; i < sections.length; i++) {
titleEl = sections[i].querySelector('.title');
@@ -66,7 +68,7 @@
element = basicFixture.instantiate();
await element.loadData();
- await flush();
+ await element.updateComplete;
});
test('renders', () => {
@@ -78,10 +80,6 @@
<gr-avatar hidden="" imagesize="120"></gr-avatar>
</span>
</section>
- <section class="hide">
- <span class="title"></span>
- <span class="value"><a href="">Change avatar</a></span>
- </section>
<section>
<span class="title">ID</span>
<span class="value">123</span>
@@ -99,20 +97,10 @@
<section id="usernameSection">
<span class="title">Username</span>
<span class="value"></span>
- <span class="value" hidden="true">
- <iron-input id="usernameIronInput">
- <input id="usernameInput" />
- </iron-input>
- </span>
</section>
<section id="nameSection">
<label class="title" for="nameInput">Full name</label>
<span class="value">User-123</span>
- <span class="value" hidden="true">
- <iron-input id="nameIronInput">
- <input id="nameInput" />
- </iron-input>
- </span>
</section>
<section>
<label class="title" for="displayNameInput">Display name</label>
@@ -127,7 +115,7 @@
About me (e.g. employer)
</label>
<span class="value">
- <iron-input>
+ <iron-input id="statusIronInput">
<input id="statusInput" />
</iron-input>
</span>
@@ -137,7 +125,7 @@
});
test('basic account info render', () => {
- assert.isFalse(element._loading);
+ assert.isFalse(element.loading);
assert.equal(valueOf('ID').textContent, `${account._account_id}`);
assert.equal(valueOf('Email').textContent, account.email);
@@ -145,55 +133,62 @@
});
test('full name render (immutable)', () => {
- const section = element.$.nameSection;
+ const section = query<HTMLElement>(element, '#nameSection')!;
const displaySpan = section.querySelectorAll('.value')[0];
const inputSpan = section.querySelectorAll('.value')[1];
assert.isFalse(element.nameMutable);
assert.isFalse(displaySpan.hasAttribute('hidden'));
assert.equal(displaySpan.textContent, account.name);
- assert.isTrue(inputSpan.hasAttribute('hidden'));
+ assert.isUndefined(inputSpan);
});
- test('full name render (mutable)', () => {
- element.set('_serverConfig', {
- auth: {editable_account_fields: ['FULL_NAME']},
- });
+ test('full name render (mutable)', async () => {
+ element.serverConfig = {
+ ...createServerInfo(),
+ auth: {
+ ...createAuth(),
+ editable_account_fields: [EditableAccountField.FULL_NAME],
+ },
+ };
- const section = element.$.nameSection;
- const displaySpan = section.querySelectorAll('.value')[0];
- const inputSpan = section.querySelectorAll('.value')[1];
+ await element.updateComplete;
+ const section = query<HTMLElement>(element, '#nameSection')!;
+ const inputSpan = section.querySelectorAll('.value')[0];
assert.isTrue(element.nameMutable);
- assert.isTrue(displaySpan.hasAttribute('hidden'));
assert.equal(queryIronInput('#nameIronInput').bindValue, account.name);
assert.isFalse(inputSpan.hasAttribute('hidden'));
});
test('username render (immutable)', () => {
- const section = element.$.usernameSection;
+ const section = query<HTMLElement>(element, '#usernameSection')!;
const displaySpan = section.querySelectorAll('.value')[0];
const inputSpan = section.querySelectorAll('.value')[1];
assert.isFalse(element.usernameMutable);
assert.isFalse(displaySpan.hasAttribute('hidden'));
assert.equal(displaySpan.textContent, account.username);
- assert.isTrue(inputSpan.hasAttribute('hidden'));
+ assert.isUndefined(inputSpan);
});
- test('username render (mutable)', () => {
- element.set('_serverConfig', {
- auth: {editable_account_fields: ['USER_NAME']},
- });
- element.set('_account.username', '');
- element.set('_username', '');
+ test('username render (mutable)', async () => {
+ element.serverConfig = {
+ ...createServerInfo(),
+ auth: {
+ ...createAuth(),
+ editable_account_fields: [EditableAccountField.USER_NAME],
+ },
+ };
+ element.account!.username = '';
+ element.username = '';
- const section = element.$.usernameSection;
- const displaySpan = section.querySelectorAll('.value')[0];
- const inputSpan = section.querySelectorAll('.value')[1];
+ await element.updateComplete;
+
+ const section = query<HTMLElement>(element, '#usernameSection')!;
+ const inputSpan = section.querySelectorAll('.value')[0];
assert.isTrue(element.usernameMutable);
- assert.isTrue(displaySpan.hasAttribute('hidden'));
assert.equal(
queryIronInput('#usernameIronInput').bindValue,
account.username
@@ -202,21 +197,23 @@
});
suite('account info edit', () => {
- let nameChangedSpy: SinonSpyMember<typeof element._nameChanged>;
- let usernameChangedSpy: SinonSpyMember<typeof element._usernameChanged>;
- let statusChangedSpy: SinonSpyMember<typeof element._statusChanged>;
let nameStub: SinonStubbedMember<RestApiService['setAccountName']>;
let usernameStub: SinonStubbedMember<RestApiService['setAccountUsername']>;
let statusStub: SinonStubbedMember<RestApiService['setAccountStatus']>;
- setup(() => {
- nameChangedSpy = sinon.spy(element, '_nameChanged');
- usernameChangedSpy = sinon.spy(element, '_usernameChanged');
- statusChangedSpy = sinon.spy(element, '_statusChanged');
- element.set('_serverConfig', {
- auth: {editable_account_fields: ['FULL_NAME', 'USER_NAME']},
- });
+ setup(async () => {
+ element.serverConfig = {
+ ...createServerInfo(),
+ auth: {
+ ...createAuth(),
+ editable_account_fields: [
+ EditableAccountField.FULL_NAME,
+ EditableAccountField.USER_NAME,
+ ],
+ },
+ };
+ await element.updateComplete;
nameStub = stubRestApi('setAccountName').resolves();
usernameStub = stubRestApi('setAccountUsername').resolves();
statusStub = stubRestApi('setAccountStatus').resolves();
@@ -226,10 +223,11 @@
assert.isTrue(element.nameMutable);
assert.isFalse(element.hasUnsavedChanges);
- element.set('_account.name', 'new name');
-
- assert.isTrue(nameChangedSpy.called);
- assert.isFalse(statusChangedSpy.called);
+ const statusInputEl = queryIronInput('#nameIronInput');
+ statusInputEl.bindValue = 'new name';
+ await element.updateComplete;
+ assert.isTrue(element.hasNameChange);
+ assert.isFalse(element.hasStatusChange);
assert.isTrue(element.hasUnsavedChanges);
await element.save();
@@ -241,14 +239,25 @@
});
test('username', async () => {
- element.set('_account.username', '');
- element._hasUsernameChange = false;
+ element.account!.username = '';
+ element.username = 't';
+ element.hasUsernameChange = false;
+ element.serverConfig = {
+ ...createServerInfo(),
+ auth: {
+ ...createAuth(),
+ editable_account_fields: [EditableAccountField.USER_NAME],
+ },
+ };
+ await element.updateComplete;
assert.isTrue(element.usernameMutable);
- element.set('_username', 'new username');
-
- assert.isTrue(usernameChangedSpy.called);
- assert.isFalse(statusChangedSpy.called);
+ const statusInputEl = queryIronInput('#usernameIronInput');
+ statusInputEl.bindValue = 'new username';
+ await element.updateComplete;
+ assert.isTrue(element.hasUsernameChange);
+ assert.isFalse(element.hasNameChange);
+ assert.isFalse(element.hasStatusChange);
assert.isTrue(element.hasUnsavedChanges);
await element.save();
@@ -262,10 +271,11 @@
test('status', async () => {
assert.isFalse(element.hasUnsavedChanges);
- element.set('_account.status', 'new status');
-
- assert.isFalse(nameChangedSpy.called);
- assert.isTrue(statusChangedSpy.called);
+ const statusInputEl = queryIronInput('#statusIronInput');
+ statusInputEl.bindValue = 'new status';
+ await element.updateComplete;
+ assert.isFalse(element.hasNameChange);
+ assert.isTrue(element.hasStatusChange);
assert.isTrue(element.hasUnsavedChanges);
await element.save();
@@ -278,17 +288,18 @@
});
suite('edit name and status', () => {
- let nameChangedSpy: SinonSpyMember<typeof element._nameChanged>;
- let statusChangedSpy: SinonSpyMember<typeof element._statusChanged>;
let nameStub: SinonStubbedMember<RestApiService['setAccountName']>;
let statusStub: SinonStubbedMember<RestApiService['setAccountStatus']>;
- setup(() => {
- nameChangedSpy = sinon.spy(element, '_nameChanged');
- statusChangedSpy = sinon.spy(element, '_statusChanged');
- element.set('_serverConfig', {
- auth: {editable_account_fields: ['FULL_NAME']},
- });
+ setup(async () => {
+ element.serverConfig = {
+ ...createServerInfo(),
+ auth: {
+ ...createAuth(),
+ editable_account_fields: [EditableAccountField.FULL_NAME],
+ },
+ };
+ await element.updateComplete;
nameStub = stubRestApi('setAccountName').resolves();
statusStub = stubRestApi('setAccountStatus').resolves();
@@ -299,13 +310,15 @@
assert.isTrue(element.nameMutable);
assert.isFalse(element.hasUnsavedChanges);
- element.set('_account.name', 'new name');
+ const inputEl = queryIronInput('#nameIronInput');
+ inputEl.bindValue = 'new name';
+ await element.updateComplete;
+ assert.isTrue(element.hasNameChange);
- assert.isTrue(nameChangedSpy.called);
-
- element.set('_account.status', 'new status');
-
- assert.isTrue(statusChangedSpy.called);
+ const statusInputEl = queryIronInput('#statusIronInput');
+ statusInputEl.bindValue = 'new status';
+ await element.updateComplete;
+ assert.isTrue(element.hasStatusChange);
assert.isTrue(element.hasUnsavedChanges);
@@ -320,18 +333,23 @@
});
suite('set status but read name', () => {
- let statusChangedSpy: SinonSpyMember<typeof element._statusChanged>;
let statusStub: SinonStubbedMember<RestApiService['setAccountStatus']>;
- setup(() => {
- statusChangedSpy = sinon.spy(element, '_statusChanged');
- element.set('_serverConfig', {auth: {editable_account_fields: []}});
+ setup(async () => {
+ element.serverConfig = {
+ ...createServerInfo(),
+ auth: {
+ ...createAuth(),
+ editable_account_fields: [],
+ },
+ };
+ await element.updateComplete;
statusStub = stubRestApi('setAccountStatus').resolves();
});
test('read full name but set status', async () => {
- const section = element.$.nameSection;
+ const section = query<HTMLElement>(element, '#nameSection')!;
const displaySpan = section.querySelectorAll('.value')[0];
const inputSpan = section.querySelectorAll('.value')[1];
@@ -341,11 +359,12 @@
assert.isFalse(displaySpan.hasAttribute('hidden'));
assert.equal(displaySpan.textContent, account.name);
- assert.isTrue(inputSpan.hasAttribute('hidden'));
+ assert.isUndefined(inputSpan);
- element.set('_account.status', 'new status');
-
- assert.isTrue(statusChangedSpy.called);
+ const inputEl = queryIronInput('#statusIronInput');
+ inputEl.bindValue = 'new status';
+ await element.updateComplete;
+ assert.isTrue(element.hasStatusChange);
assert.isTrue(element.hasUnsavedChanges);
@@ -356,27 +375,27 @@
});
});
- test('_usernameChanged compares usernames with loose equality', () => {
- element._account = createAccountDetailWithId();
- element._username = '';
- element._hasUsernameChange = false;
- element._loading = false;
- // _usernameChanged is an observer, but call it here after setting
- // _hasUsernameChange in the test to force recomputation.
- element._usernameChanged();
- flush();
+ test('_usernameChanged compares usernames with loose equality', async () => {
+ element.serverConfig = {
+ ...createServerInfo(),
+ auth: {
+ ...createAuth(),
+ editable_account_fields: [EditableAccountField.USER_NAME],
+ },
+ };
+ element.account = createAccountDetailWithId();
+ element.username = 't';
+ element.hasUsernameChange = false;
+ element.loading = false;
+ // usernameChanged is an observer, but call it here after setting
+ // hasUsernameChange in the test to force recomputation.
+ await element.updateComplete;
+ assert.isFalse(element.hasUsernameChange);
- assert.isFalse(element._hasUsernameChange);
+ const inputEl = queryIronInput('#usernameIronInput');
+ inputEl.bindValue = 'test';
+ await element.updateComplete;
- element.set('_username', 'test');
- flush();
-
- assert.isTrue(element._hasUsernameChange);
- });
-
- test('_hideAvatarChangeUrl', () => {
- assert.equal(element._hideAvatarChangeUrl(''), 'hide');
-
- assert.equal(element._hideAvatarChangeUrl('https://example.com'), '');
+ assert.isTrue(element.hasUsernameChange);
});
});
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
index 7a8a771..ca3f11d 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
@@ -16,12 +16,6 @@
*/
import '@polymer/iron-input/iron-input';
import '@polymer/paper-toggle-button/paper-toggle-button';
-import '../../../styles/gr-font-styles';
-import '../../../styles/gr-form-styles';
-import '../../../styles/gr-menu-page-styles';
-import '../../../styles/gr-page-nav-styles';
-import '../../../styles/gr-paper-styles';
-import '../../../styles/shared-styles';
import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
import '../gr-change-table-editor/gr-change-table-editor';
import '../../shared/gr-button/gr-button';
@@ -39,10 +33,7 @@
import '../gr-menu-editor/gr-menu-editor';
import '../gr-ssh-editor/gr-ssh-editor';
import '../gr-watched-projects-editor/gr-watched-projects-editor';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-settings-view_html';
import {getDocsBaseUrl} from '../../../utils/url-util';
-import {customElement, property, observe} from '@polymer/decorators';
import {AppElementParams} from '../../gr-app-types';
import {GrAccountInfo} from '../gr-account-info/gr-account-info';
import {GrWatchedProjectsEditor} from '../gr-watched-projects-editor/gr-watched-projects-editor';
@@ -66,24 +57,16 @@
} from '../../../constants/constants';
import {columnNames} from '../../change-list/gr-change-list/gr-change-list';
import {windowLocationReload} from '../../../utils/dom-util';
-import {ValueChangedEvent} from '../../../types/events';
-
-const PREFS_SECTION_FIELDS: Array<keyof PreferencesInput> = [
- 'changes_per_page',
- 'date_format',
- 'time_format',
- 'email_strategy',
- 'diff_view',
- 'publish_comments_on_push',
- 'disable_keyboard_shortcuts',
- 'disable_token_highlighting',
- 'work_in_progress_by_default',
- 'default_base_for_merges',
- 'signed_off_by',
- 'email_format',
- 'size_bar_in_change_table',
- 'relative_date_in_change_table',
-];
+import {BindValueChangeEvent, ValueChangedEvent} from '../../../types/events';
+import {LitElement, css, html} from 'lit';
+import {customElement, property, query, state} from 'lit/decorators';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {paperStyles} from '../../../styles/gr-paper-styles';
+import {fontStyles} from '../../../styles/gr-font-styles';
+import {when} from 'lit/directives/when';
+import {pageNavStyles} from '../../../styles/gr-page-nav-styles';
+import {menuPageStyles} from '../../../styles/gr-menu-page-styles';
+import {formStyles} from '../../../styles/gr-form-styles';
const GERRIT_DOCS_BASE_URL =
'https://gerrit-review.googlesource.com/' + 'Documentation';
@@ -98,39 +81,8 @@
LocalPrefsToPrefs,
}
-export interface GrSettingsView {
- $: {
- accountInfo: GrAccountInfo;
- watchedProjectsEditor: GrWatchedProjectsEditor;
- groupList: GrGroupList;
- identities: GrIdentities;
- diffPrefs: GrDiffPreferences;
- sshEditor: GrSshEditor;
- gpgEditor: GrGpgEditor;
- emailEditor: GrEmailEditor;
- insertSignedOff: HTMLInputElement;
- workInProgressByDefault: HTMLInputElement;
- showSizeBarsInFileList: HTMLInputElement;
- publishCommentsOnPush: HTMLInputElement;
- disableKeyboardShortcuts: HTMLInputElement;
- disableTokenHighlighting: HTMLInputElement;
- relativeDateInChangeTable: HTMLInputElement;
- changesPerPageSelect: HTMLInputElement;
- dateTimeFormatSelect: HTMLInputElement;
- timeFormatSelect: HTMLInputElement;
- emailNotificationsSelect: HTMLInputElement;
- emailFormatSelect: HTMLInputElement;
- defaultBaseForMergesSelect: HTMLInputElement;
- diffViewSelect: HTMLInputElement;
- };
-}
-
@customElement('gr-settings-view')
-export class GrSettingsView extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
-
+export class GrSettingsView extends LitElement {
/**
* Fired when the title of the page should change.
*
@@ -143,66 +95,106 @@
* @event show-alert
*/
- @property({type: Object})
- prefs: PreferencesInput = {};
+ @query('#accountInfo', true) accountInfo!: GrAccountInfo;
- @property({type: Object})
- params?: AppElementParams;
+ @query('#watchedProjectsEditor', true)
+ watchedProjectsEditor!: GrWatchedProjectsEditor;
- @property({type: Boolean})
- _accountInfoChanged?: boolean;
+ @query('#groupList', true) groupList!: GrGroupList;
- @property({type: Object})
- _localPrefs: PreferencesInput = {};
+ @query('#identities', true) identities!: GrIdentities;
- @property({type: Array})
- _localChangeTableColumns: string[] = [];
+ @query('#diffPrefs') diffPrefs!: GrDiffPreferences;
- @property({type: Boolean})
- _loading = true;
+ @query('#sshEditor') sshEditor?: GrSshEditor;
- @property({type: Boolean})
- _changeTableChanged = false;
+ @query('#gpgEditor') gpgEditor?: GrGpgEditor;
- @property({type: Boolean})
- _prefsChanged = false;
+ @query('#emailEditor', true) emailEditor!: GrEmailEditor;
- @property({type: Boolean})
- _diffPrefsChanged = false;
+ @query('#insertSignedOff') insertSignedOff!: HTMLInputElement;
- @property({type: Boolean})
- _watchedProjectsChanged = false;
+ @query('#workInProgressByDefault') workInProgressByDefault!: HTMLInputElement;
- @property({type: Boolean})
- _keysChanged = false;
+ @query('#showSizeBarsInFileList') showSizeBarsInFileList!: HTMLInputElement;
- @property({type: Boolean})
- _gpgKeysChanged = false;
+ @query('#publishCommentsOnPush') publishCommentsOnPush!: HTMLInputElement;
- @property({type: String})
- _newEmail?: string;
+ @query('#disableKeyboardShortcuts')
+ disableKeyboardShortcuts!: HTMLInputElement;
- @property({type: Boolean})
- _addingEmail = false;
+ @query('#disableTokenHighlighting')
+ disableTokenHighlighting!: HTMLInputElement;
- @property({type: String})
- _lastSentVerificationEmail?: string | null = null;
+ @query('#relativeDateInChangeTable')
+ relativeDateInChangeTable!: HTMLInputElement;
- @property({type: Object})
- _serverConfig?: ServerInfo;
+ @query('#changesPerPageSelect') changesPerPageSelect!: HTMLInputElement;
- @property({type: String})
- _docsBaseUrl?: string | null;
+ @query('#dateTimeFormatSelect') dateTimeFormatSelect!: HTMLInputElement;
- @property({type: Boolean})
- _emailsChanged = false;
+ @query('#timeFormatSelect') timeFormatSelect!: HTMLInputElement;
- @property({type: Boolean})
- _showNumber?: boolean;
+ @query('#emailNotificationsSelect')
+ emailNotificationsSelect!: HTMLInputElement;
- @property({type: Boolean})
- _isDark = false;
+ @query('#emailFormatSelect') emailFormatSelect!: HTMLInputElement;
+ @query('#defaultBaseForMergesSelect')
+ defaultBaseForMergesSelect!: HTMLInputElement;
+
+ @query('#diffViewSelect') diffViewSelect!: HTMLInputElement;
+
+ @state() prefs: PreferencesInput = {};
+
+ @property({type: Object}) params?: AppElementParams;
+
+ @state() private accountInfoChanged = false;
+
+ @state() private localPrefs: PreferencesInput = {};
+
+ // private but used in test
+ @state() localChangeTableColumns: string[] = [];
+
+ @state() private loading = true;
+
+ @state() private changeTableChanged = false;
+
+ // private but used in test
+ @state() prefsChanged = false;
+
+ @state() private diffPrefsChanged = false;
+
+ @state() private watchedProjectsChanged = false;
+
+ @state() private keysChanged = false;
+
+ @state() private gpgKeysChanged = false;
+
+ // private but used in test
+ @state() newEmail?: string;
+
+ // private but used in test
+ @state() addingEmail = false;
+
+ // private but used in test
+ @state() lastSentVerificationEmail?: string | null = null;
+
+ // private but used in test
+ @state() serverConfig?: ServerInfo;
+
+ // private but used in test
+ @state() docsBaseUrl?: string | null;
+
+ @state() private emailsChanged = false;
+
+ // private but used in test
+ @state() showNumber?: boolean;
+
+ // private but used in test
+ @state() isDark = false;
+
+ // private but used in test
public _testOnly_loadingPromise?: Promise<void>;
private readonly restApiService = getAppContext().restApiService;
@@ -213,14 +205,16 @@
// we need to manually calling scrollIntoView when hash changed
window.addEventListener('location-change', this.handleLocationChange);
fireTitleChange(this, 'Settings');
+ }
- this._isDark = !!window.localStorage.getItem('dark-theme');
+ override firstUpdated() {
+ this.isDark = !!window.localStorage.getItem('dark-theme');
const promises: Array<Promise<unknown>> = [
- this.$.accountInfo.loadData(),
- this.$.watchedProjectsEditor.loadData(),
- this.$.groupList.loadData(),
- this.$.identities.loadData(),
+ this.accountInfo.loadData(),
+ this.watchedProjectsEditor.loadData(),
+ this.groupList.loadData(),
+ this.identities.loadData(),
];
// TODO(dhruvsri): move this to the service
@@ -230,9 +224,9 @@
throw new Error('getPreferences returned undefined');
}
this.prefs = prefs;
- this._showNumber = !!prefs.legacycid_in_change_table;
- this._copyPrefs(CopyPrefsDirection.PrefsToLocalPrefs);
- this._localChangeTableColumns =
+ this.showNumber = !!prefs.legacycid_in_change_table;
+ this.copyPrefs(CopyPrefsDirection.PrefsToLocalPrefs);
+ this.localChangeTableColumns =
prefs.change_table.length === 0
? columnNames
: prefs.change_table.map(column =>
@@ -243,24 +237,20 @@
promises.push(
this.restApiService.getConfig().then(config => {
- this._serverConfig = config;
+ this.serverConfig = config;
const configPromises: Array<Promise<void>> = [];
- if (this._serverConfig && this._serverConfig.sshd) {
- configPromises.push(this.$.sshEditor.loadData());
+ if (this.serverConfig?.sshd && this.sshEditor) {
+ configPromises.push(this.sshEditor.loadData());
}
- if (
- this._serverConfig &&
- this._serverConfig.receive &&
- this._serverConfig.receive.enable_signed_push
- ) {
- configPromises.push(this.$.gpgEditor.loadData());
+ if (this.serverConfig?.receive?.enable_signed_push && this.gpgEditor) {
+ configPromises.push(this.gpgEditor.loadData());
}
configPromises.push(
getDocsBaseUrl(config, this.restApiService).then(baseUrl => {
- this._docsBaseUrl = baseUrl;
+ this.docsBaseUrl = baseUrl;
})
);
@@ -280,34 +270,728 @@
if (message) {
fireAlert(this, message);
}
- this.$.emailEditor.loadData();
+ this.emailEditor.loadData();
})
);
} else {
- promises.push(this.$.emailEditor.loadData());
+ promises.push(this.emailEditor.loadData());
}
this._testOnly_loadingPromise = Promise.all(promises).then(() => {
- this._loading = false;
+ this.loading = false;
// Handle anchor tag for initial load
this.handleLocationChange();
});
}
+ static override styles = [
+ sharedStyles,
+ paperStyles,
+ fontStyles,
+ formStyles,
+ menuPageStyles,
+ pageNavStyles,
+ css`
+ :host {
+ color: var(--primary-text-color);
+ }
+ h2 {
+ font-family: var(--header-font-family);
+ font-size: var(--font-size-h2);
+ font-weight: var(--font-weight-h2);
+ line-height: var(--line-height-h2);
+ }
+ .newEmailInput {
+ width: 20em;
+ }
+ #email {
+ margin-bottom: var(--spacing-l);
+ }
+ .main section.darkToggle {
+ display: block;
+ }
+ .filters p,
+ .darkToggle p {
+ margin-bottom: var(--spacing-l);
+ }
+ .queryExample em {
+ color: violet;
+ }
+ .toggle {
+ align-items: center;
+ display: flex;
+ margin-bottom: var(--spacing-l);
+ margin-right: var(--spacing-l);
+ }
+ `,
+ ];
+
+ override render() {
+ const isLoading = this.loading || this.loading === undefined;
+ return html`<div class="loading" ?hidden=${!isLoading}>Loading...</div>
+ <div ?hidden=${isLoading}>
+ <gr-page-nav class="navStyles">
+ <ul>
+ <li><a href="#Profile">Profile</a></li>
+ <li><a href="#Preferences">Preferences</a></li>
+ <li><a href="#DiffPreferences">Diff Preferences</a></li>
+ <li><a href="#EditPreferences">Edit Preferences</a></li>
+ <li><a href="#Menu">Menu</a></li>
+ <li><a href="#ChangeTableColumns">Change Table Columns</a></li>
+ <li><a href="#Notifications">Notifications</a></li>
+ <li><a href="#EmailAddresses">Email Addresses</a></li>
+ ${when(
+ this.showHttpAuth(),
+ () =>
+ html`<li><a href="#HTTPCredentials">HTTP Credentials</a></li>`
+ )}
+ ${when(
+ this.serverConfig?.sshd,
+ () => html`<li><a href="#SSHKeys"> SSH Keys </a></li>`
+ )}
+ ${when(
+ this.serverConfig?.receive?.enable_signed_push,
+ () => html`<li><a href="#GPGKeys"> GPG Keys </a></li>`
+ )}
+ <li><a href="#Groups">Groups</a></li>
+ <li><a href="#Identities">Identities</a></li>
+ ${when(
+ this.serverConfig?.auth.use_contributor_agreements,
+ () => html`<li><a href="#Agreements">Agreements</a></li>`
+ )}
+ <li><a href="#MailFilters">Mail Filters</a></li>
+ <gr-endpoint-decorator name="settings-menu-item">
+ </gr-endpoint-decorator>
+ </ul>
+ </gr-page-nav>
+ <div class="main gr-form-styles">
+ <h1 class="heading-1">User Settings</h1>
+ <h2 id="Theme">Theme</h2>
+ <section class="darkToggle">
+ <div class="toggle">
+ <paper-toggle-button
+ aria-labelledby="darkThemeToggleLabel"
+ ?checked=${this.isDark}
+ @change=${this.handleToggleDark}
+ @click=${this.onTapDarkToggle}
+ ></paper-toggle-button>
+ <div id="darkThemeToggleLabel">
+ Dark theme (the toggle reloads the page)
+ </div>
+ </div>
+ </section>
+ <h2
+ id="Profile"
+ class=${this.computeHeaderClass(this.accountInfoChanged)}
+ >
+ Profile
+ </h2>
+ <fieldset id="profile">
+ <gr-account-info
+ id="accountInfo"
+ ?hasUnsavedChanges=${this.accountInfoChanged}
+ @unsaved-changes-changed=${(e: ValueChangedEvent<boolean>) => {
+ this.accountInfoChanged = e.detail.value;
+ }}
+ ></gr-account-info>
+ <gr-button
+ @click=${() => {
+ this.accountInfo.save();
+ }}
+ ?disabled=${!this.accountInfoChanged}
+ >Save changes</gr-button
+ >
+ </fieldset>
+ <h2
+ id="Preferences"
+ class=${this.computeHeaderClass(this.prefsChanged)}
+ >
+ Preferences
+ </h2>
+ <fieldset id="preferences">
+ <section>
+ <label class="title" for="changesPerPageSelect"
+ >Changes per page</label
+ >
+ <span class="value">
+ <gr-select
+ .bindValue=${this.convertToString(
+ this.localPrefs.changes_per_page
+ )}
+ @change=${() => {
+ this.localPrefs.changes_per_page = Number(
+ this.changesPerPageSelect.value
+ ) as 10 | 25 | 50 | 100;
+ this.prefsChanged = true;
+ }}
+ >
+ <select id="changesPerPageSelect">
+ <option value="10">10 rows per page</option>
+ <option value="25">25 rows per page</option>
+ <option value="50">50 rows per page</option>
+ <option value="100">100 rows per page</option>
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ <section>
+ <label class="title" for="dateTimeFormatSelect"
+ >Date/time format</label
+ >
+ <span class="value">
+ <gr-select
+ .bindValue=${this.convertToString(
+ this.localPrefs.date_format
+ )}
+ @change=${() => {
+ this.localPrefs.date_format = this.dateTimeFormatSelect
+ .value as DateFormat;
+ this.prefsChanged = true;
+ }}
+ >
+ <select id="dateTimeFormatSelect">
+ <option value="STD">Jun 3 ; Jun 3, 2016</option>
+ <option value="US">06/03 ; 06/03/16</option>
+ <option value="ISO">06-03 ; 2016-06-03</option>
+ <option value="EURO">3. Jun ; 03.06.2016</option>
+ <option value="UK">03/06 ; 03/06/2016</option>
+ </select>
+ </gr-select>
+ <gr-select
+ .bindValue=${this.convertToString(
+ this.localPrefs.time_format
+ )}
+ aria-label="Time Format"
+ @change=${() => {
+ this.localPrefs.time_format = this.timeFormatSelect
+ .value as TimeFormat;
+ this.prefsChanged = true;
+ }}
+ >
+ <select id="timeFormatSelect">
+ <option value="HHMM_12">4:10 PM</option>
+ <option value="HHMM_24">16:10</option>
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ <section>
+ <label class="title" for="emailNotificationsSelect"
+ >Email notifications</label
+ >
+ <span class="value">
+ <gr-select
+ .bindValue=${this.convertToString(
+ this.localPrefs.email_strategy
+ )}
+ @change=${() => {
+ this.localPrefs.email_strategy = this
+ .emailNotificationsSelect.value as EmailStrategy;
+ this.prefsChanged = true;
+ }}
+ >
+ <select id="emailNotificationsSelect">
+ <option value="CC_ON_OWN_COMMENTS">Every comment</option>
+ <option value="ENABLED">
+ Only comments left by others
+ </option>
+ <option value="ATTENTION_SET_ONLY">
+ Only when I am in the attention set
+ </option>
+ <option value="DISABLED">None</option>
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ <section
+ ?hidden=${!this.convertToString(this.localPrefs.email_format)}
+ >
+ <label class="title" for="emailFormatSelect">Email format</label>
+ <span class="value">
+ <gr-select
+ .bindValue=${this.convertToString(
+ this.localPrefs.email_format
+ )}
+ @change=${() => {
+ this.localPrefs.email_format = this.emailFormatSelect
+ .value as EmailFormat;
+ this.prefsChanged = true;
+ }}
+ >
+ <select id="emailFormatSelect">
+ <option value="HTML_PLAINTEXT">HTML and plaintext</option>
+ <option value="PLAINTEXT">Plaintext only</option>
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ <section ?hidden=${!this.localPrefs.default_base_for_merges}>
+ <span class="title">Default Base For Merges</span>
+ <span class="value">
+ <gr-select
+ .bindValue=${this.convertToString(
+ this.localPrefs.default_base_for_merges
+ )}
+ @change=${() => {
+ this.localPrefs.default_base_for_merges = this
+ .defaultBaseForMergesSelect.value as DefaultBase;
+ this.prefsChanged = true;
+ }}
+ >
+ <select id="defaultBaseForMergesSelect">
+ <option value="AUTO_MERGE">Auto Merge</option>
+ <option value="FIRST_PARENT">First Parent</option>
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ <section>
+ <label class="title" for="relativeDateInChangeTable"
+ >Show Relative Dates In Changes Table</label
+ >
+ <span class="value">
+ <input
+ id="relativeDateInChangeTable"
+ type="checkbox"
+ ?checked=${this.localPrefs.relative_date_in_change_table}
+ @change=${() => {
+ this.localPrefs.relative_date_in_change_table =
+ this.relativeDateInChangeTable.checked;
+ this.prefsChanged = true;
+ }}
+ />
+ </span>
+ </section>
+ <section>
+ <span class="title">Diff view</span>
+ <span class="value">
+ <gr-select
+ .bindValue=${this.convertToString(this.localPrefs.diff_view)}
+ @change=${() => {
+ this.localPrefs.diff_view = this.diffViewSelect
+ .value as DiffViewMode;
+ this.prefsChanged = true;
+ }}
+ >
+ <select id="diffViewSelect">
+ <option value="SIDE_BY_SIDE">Side by side</option>
+ <option value="UNIFIED_DIFF">Unified diff</option>
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ <section>
+ <label for="showSizeBarsInFileList" class="title"
+ >Show size bars in file list</label
+ >
+ <span class="value">
+ <input
+ id="showSizeBarsInFileList"
+ type="checkbox"
+ ?checked=${this.localPrefs.size_bar_in_change_table}
+ @change=${() => {
+ this.localPrefs.size_bar_in_change_table =
+ this.showSizeBarsInFileList.checked;
+ this.prefsChanged = true;
+ }}
+ />
+ </span>
+ </section>
+ <section>
+ <label for="publishCommentsOnPush" class="title"
+ >Publish comments on push</label
+ >
+ <span class="value">
+ <input
+ id="publishCommentsOnPush"
+ type="checkbox"
+ ?checked=${this.localPrefs.publish_comments_on_push}
+ @change=${() => {
+ this.localPrefs.publish_comments_on_push =
+ this.publishCommentsOnPush.checked;
+ this.prefsChanged = true;
+ }}
+ />
+ </span>
+ </section>
+ <section>
+ <label for="workInProgressByDefault" class="title"
+ >Set new changes to "work in progress" by default</label
+ >
+ <span class="value">
+ <input
+ id="workInProgressByDefault"
+ type="checkbox"
+ ?checked=${this.localPrefs.work_in_progress_by_default}
+ @change=${() => {
+ this.localPrefs.work_in_progress_by_default =
+ this.workInProgressByDefault.checked;
+ this.prefsChanged = true;
+ }}
+ />
+ </span>
+ </section>
+ <section>
+ <label for="disableKeyboardShortcuts" class="title"
+ >Disable all keyboard shortcuts</label
+ >
+ <span class="value">
+ <input
+ id="disableKeyboardShortcuts"
+ type="checkbox"
+ ?checked=${this.localPrefs.disable_keyboard_shortcuts}
+ @change=${() => {
+ this.localPrefs.disable_keyboard_shortcuts =
+ this.disableKeyboardShortcuts.checked;
+ this.prefsChanged = true;
+ }}
+ />
+ </span>
+ </section>
+ <section>
+ <label for="disableTokenHighlighting" class="title"
+ >Disable token highlighting on hover</label
+ >
+ <span class="value">
+ <input
+ id="disableTokenHighlighting"
+ type="checkbox"
+ ?checked=${this.localPrefs.disable_token_highlighting}
+ @change=${() => {
+ this.localPrefs.disable_token_highlighting =
+ this.disableTokenHighlighting.checked;
+ this.prefsChanged = true;
+ }}
+ />
+ </span>
+ </section>
+ <section>
+ <label for="insertSignedOff" class="title">
+ Insert Signed-off-by Footer For Inline Edit Changes
+ </label>
+ <span class="value">
+ <input
+ id="insertSignedOff"
+ type="checkbox"
+ ?checked=${this.localPrefs.signed_off_by}
+ @change=${() => {
+ this.localPrefs.signed_off_by =
+ this.insertSignedOff.checked;
+ this.prefsChanged = true;
+ }}
+ />
+ </span>
+ </section>
+ <gr-button
+ id="savePrefs"
+ @click=${this.handleSavePreferences}
+ ?disabled=${!this.prefsChanged}
+ >Save changes</gr-button
+ >
+ </fieldset>
+ <h2
+ id="DiffPreferences"
+ class=${this.computeHeaderClass(this.diffPrefsChanged)}
+ >
+ Diff Preferences
+ </h2>
+ <fieldset id="diffPreferences">
+ <gr-diff-preferences
+ id="diffPrefs"
+ @has-unsaved-changes-changed=${(
+ e: ValueChangedEvent<boolean>
+ ) => {
+ this.diffPrefsChanged = e.detail.value;
+ }}
+ ></gr-diff-preferences>
+ <gr-button
+ id="saveDiffPrefs"
+ @click=${() => {
+ this.diffPrefs.save();
+ }}
+ ?disabled=${!this.diffPrefsChanged}
+ >Save changes</gr-button
+ >
+ </fieldset>
+ <gr-edit-preferences id="editPrefs"></gr-edit-preferences>
+ <gr-menu-editor></gr-menu-editor>
+ <h2
+ id="ChangeTableColumns"
+ class=${this.computeHeaderClass(this.changeTableChanged)}
+ >
+ Change Table Columns
+ </h2>
+ <fieldset id="changeTableColumns">
+ <gr-change-table-editor
+ .showNumber=${this.showNumber}
+ @show-number-changed=${(e: ValueChangedEvent<boolean>) => {
+ this.showNumber = e.detail.value;
+ this.changeTableChanged = true;
+ }}
+ .serverConfig=${this.serverConfig}
+ .defaultColumns=${this.localChangeTableColumns}
+ @displayed-columns-changed=${(e: ValueChangedEvent<string[]>) => {
+ this.localChangeTableColumns = e.detail.value;
+ this.changeTableChanged = true;
+ }}
+ >
+ </gr-change-table-editor>
+ <gr-button
+ id="saveChangeTable"
+ @click=${this.handleSaveChangeTable}
+ ?disabled=${!this.changeTableChanged}
+ >Save changes</gr-button
+ >
+ </fieldset>
+ <h2
+ id="Notifications"
+ class=${this.computeHeaderClass(this.watchedProjectsChanged)}
+ >
+ Notifications
+ </h2>
+ <fieldset id="watchedProjects">
+ <gr-watched-projects-editor
+ ?hasUnsavedChanges=${this.watchedProjectsChanged}
+ @has-unsaved-changes-changed=${(
+ e: ValueChangedEvent<boolean>
+ ) => {
+ this.watchedProjectsChanged = e.detail.value;
+ }}
+ id="watchedProjectsEditor"
+ ></gr-watched-projects-editor>
+ <gr-button
+ @click=${() => {
+ this.watchedProjectsEditor.save();
+ }}
+ ?disabled=${!this.watchedProjectsChanged}
+ id="_handleSaveWatchedProjects"
+ >Save changes</gr-button
+ >
+ </fieldset>
+ <h2
+ id="EmailAddresses"
+ class=${this.computeHeaderClass(this.emailsChanged)}
+ >
+ Email Addresses
+ </h2>
+ <fieldset id="email">
+ <gr-email-editor
+ id="emailEditor"
+ ?hasUnsavedChanges=${this.emailsChanged}
+ @has-unsaved-changes-changed=${(
+ e: ValueChangedEvent<boolean>
+ ) => {
+ this.emailsChanged = e.detail.value;
+ }}
+ ></gr-email-editor>
+ <gr-button
+ @click=${() => {
+ this.emailEditor.save();
+ }}
+ ?disabled=${!this.emailsChanged}
+ >Save changes</gr-button
+ >
+ </fieldset>
+ <fieldset id="newEmail">
+ <section>
+ <span class="title">New email address</span>
+ <span class="value">
+ <iron-input
+ class="newEmailInput"
+ .bindValue=${this.newEmail}
+ @bind-value-changed=${(e: BindValueChangeEvent) => {
+ this.newEmail = e.detail.value;
+ }}
+ @keydown=${this.handleNewEmailKeydown}
+ >
+ <input
+ class="newEmailInput"
+ type="text"
+ ?disabled=${this.addingEmail}
+ @keydown=${this.handleNewEmailKeydown}
+ placeholder="email@example.com"
+ />
+ </iron-input>
+ </span>
+ </section>
+ <section
+ id="verificationSentMessage"
+ ?hidden=${!this.lastSentVerificationEmail}
+ >
+ <p>
+ A verification email was sent to
+ <em>${this.lastSentVerificationEmail}</em>. Please check your
+ inbox.
+ </p>
+ </section>
+ <gr-button
+ ?disabled=${!this.computeAddEmailButtonEnabled()}
+ @click=${this.handleAddEmailButton}
+ >Send verification</gr-button
+ >
+ </fieldset>
+ ${when(
+ this.showHttpAuth(),
+ () => html` <div>
+ <h2 id="HTTPCredentials">HTTP Credentials</h2>
+ <fieldset>
+ <gr-http-password id="httpPass"></gr-http-password>
+ </fieldset>
+ </div>`
+ )}
+ ${when(
+ this.serverConfig?.sshd,
+ () => html`<h2
+ id="SSHKeys"
+ class=${this.computeHeaderClass(this.keysChanged)}
+ >
+ SSH keys
+ </h2>
+ <gr-ssh-editor
+ id="sshEditor"
+ ?hasUnsavedChanges=${this.keysChanged}
+ @has-unsaved-changes-changed=${(
+ e: ValueChangedEvent<boolean>
+ ) => {
+ this.keysChanged = e.detail.value;
+ }}
+ ></gr-ssh-editor>`
+ )}
+ ${when(
+ this.serverConfig?.receive?.enable_signed_push,
+ () => html`<div>
+ <h2
+ id="GPGKeys"
+ class=${this.computeHeaderClass(this.gpgKeysChanged)}
+ >
+ GPG keys
+ </h2>
+ <gr-gpg-editor
+ id="gpgEditor"
+ ?hasUnsavedChanges=${this.gpgKeysChanged}
+ @has-unsaved-changes-changed=${(
+ e: ValueChangedEvent<boolean>
+ ) => {
+ this.gpgKeysChanged = e.detail.value;
+ }}
+ ></gr-gpg-editor>
+ </div>`
+ )}
+ <h2 id="Groups">Groups</h2>
+ <fieldset>
+ <gr-group-list id="groupList"></gr-group-list>
+ </fieldset>
+ <h2 id="Identities">Identities</h2>
+ <fieldset>
+ <gr-identities
+ id="identities"
+ .serverConfig=${this.serverConfig}
+ ></gr-identities>
+ </fieldset>
+ ${when(
+ this.serverConfig?.auth.use_contributor_agreements,
+ () => html`<h2 id="Agreements">Agreements</h2>
+ <fieldset>
+ <gr-agreements-list id="agreementsList"></gr-agreements-list>
+ </fieldset>`
+ )}
+ <h2 id="MailFilters">Mail Filters</h2>
+ <fieldset class="filters">
+ <p>
+ Gerrit emails include metadata about the change to support writing
+ mail filters.
+ </p>
+ <p>
+ Here are some example Gmail queries that can be used for filters
+ or for searching through archived messages. View the
+ <a
+ href=${this.getFilterDocsLink(this.docsBaseUrl)}
+ target="_blank"
+ rel="nofollow"
+ >Gerrit documentation</a
+ >
+ for the complete set of footers.
+ </p>
+ <table>
+ <tbody>
+ <tr>
+ <th>Name</th>
+ <th>Query</th>
+ </tr>
+ <tr>
+ <td>Changes requesting my review</td>
+ <td>
+ <code class="queryExample">
+ "Gerrit-Reviewer: <em>Your Name</em>
+ <<em>your.email@example.com</em>>"
+ </code>
+ </td>
+ </tr>
+ <tr>
+ <td>Changes requesting my attention</td>
+ <td>
+ <code class="queryExample">
+ "Gerrit-Attention: <em>Your Name</em>
+ <<em>your.email@example.com</em>>"
+ </code>
+ </td>
+ </tr>
+ <tr>
+ <td>Changes from a specific owner</td>
+ <td>
+ <code class="queryExample">
+ "Gerrit-Owner: <em>Owner name</em>
+ <<em>owner.email@example.com</em>>"
+ </code>
+ </td>
+ </tr>
+ <tr>
+ <td>Changes targeting a specific branch</td>
+ <td>
+ <code class="queryExample">
+ "Gerrit-Branch: <em>branch-name</em>"
+ </code>
+ </td>
+ </tr>
+ <tr>
+ <td>Changes in a specific project</td>
+ <td>
+ <code class="queryExample">
+ "Gerrit-Project: <em>project-name</em>"
+ </code>
+ </td>
+ </tr>
+ <tr>
+ <td>Messages related to a specific Change ID</td>
+ <td>
+ <code class="queryExample">
+ "Gerrit-Change-Id: <em>Change ID</em>"
+ </code>
+ </td>
+ </tr>
+ <tr>
+ <td>Messages related to a specific change number</td>
+ <td>
+ <code class="queryExample">
+ "Gerrit-Change-Number: <em>change number</em>"
+ </code>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </fieldset>
+ <gr-endpoint-decorator name="settings-screen">
+ </gr-endpoint-decorator>
+ </div>
+ </div>`;
+ }
+
override disconnectedCallback() {
window.removeEventListener('location-change', this.handleLocationChange);
super.disconnectedCallback();
}
- handleUnsavedChangesChanged(e: ValueChangedEvent) {
- this._keysChanged = !!e.detail.value;
- }
-
- _handleGpgEditorHasSavedChanges(e: ValueChangedEvent<boolean>) {
- this._gpgKeysChanged = e.detail.value;
- }
-
private readonly handleLocationChange = () => {
// Handle anchor tag after dom attached
const urlHash = window.location.hash;
@@ -321,176 +1005,82 @@
};
reloadAccountDetail() {
- Promise.all([this.$.accountInfo.loadData(), this.$.emailEditor.loadData()]);
+ Promise.all([this.accountInfo.loadData(), this.emailEditor.loadData()]);
}
- _isLoading() {
- return this._loading || this._loading === undefined;
- }
-
- _copyPrefs(direction: CopyPrefsDirection) {
- let to;
- let from;
+ private copyPrefs(direction: CopyPrefsDirection) {
if (direction === CopyPrefsDirection.LocalPrefsToPrefs) {
- from = this._localPrefs;
- to = 'prefs';
+ this.prefs = {
+ ...this.localPrefs,
+ };
} else {
- from = this.prefs;
- to = '_localPrefs';
- }
- for (let i = 0; i < PREFS_SECTION_FIELDS.length; i++) {
- this.set([to, PREFS_SECTION_FIELDS[i]], from[PREFS_SECTION_FIELDS[i]]);
+ this.localPrefs = {
+ ...this.prefs,
+ };
}
}
- @observe('_localChangeTableColumns', '_showNumber')
- _handleChangeTableChanged() {
- if (this._isLoading()) {
- return;
- }
- this._changeTableChanged = true;
- }
-
- @observe('_localPrefs.*')
- _handlePrefsChanged() {
- if (this._isLoading()) {
- return;
- }
- this._prefsChanged = true;
- }
-
- _handleRelativeDateInChangeTable() {
- this.set(
- '_localPrefs.relative_date_in_change_table',
- this.$.relativeDateInChangeTable.checked
- );
- }
-
- _handleShowSizeBarsInFileListChanged() {
- this.set(
- '_localPrefs.size_bar_in_change_table',
- this.$.showSizeBarsInFileList.checked
- );
- }
-
- _handlePublishCommentsOnPushChanged() {
- this.set(
- '_localPrefs.publish_comments_on_push',
- this.$.publishCommentsOnPush.checked
- );
- }
-
- _handleDisableKeyboardShortcutsChanged() {
- this.set(
- '_localPrefs.disable_keyboard_shortcuts',
- this.$.disableKeyboardShortcuts.checked
- );
- }
-
- _handleDisableTokenHighlightingChanged() {
- this.set(
- '_localPrefs.disable_token_highlighting',
- this.$.disableTokenHighlighting.checked
- );
- }
-
- _handleWorkInProgressByDefault() {
- this.set(
- '_localPrefs.work_in_progress_by_default',
- this.$.workInProgressByDefault.checked
- );
- }
-
- _handleInsertSignedOff() {
- this.set('_localPrefs.signed_off_by', this.$.insertSignedOff.checked);
- }
-
- _handleSaveAccountInfo() {
- this.$.accountInfo.save();
- }
-
- _handleSavePreferences() {
- this._copyPrefs(CopyPrefsDirection.LocalPrefsToPrefs);
+ // private but used in test
+ handleSavePreferences() {
+ this.copyPrefs(CopyPrefsDirection.LocalPrefsToPrefs);
return this.restApiService.savePreferences(this.prefs).then(() => {
- this._prefsChanged = false;
+ this.prefsChanged = false;
});
}
- _handleSaveChangeTable() {
- this.set('prefs.change_table', this._localChangeTableColumns);
- this.set('prefs.legacycid_in_change_table', this._showNumber);
+ // private but used in test
+ handleSaveChangeTable() {
+ this.prefs.change_table = this.localChangeTableColumns;
+ this.prefs.legacycid_in_change_table = this.showNumber;
return this.restApiService.savePreferences(this.prefs).then(() => {
- this._changeTableChanged = false;
+ this.changeTableChanged = false;
});
}
- _handleSaveDiffPreferences() {
- this.$.diffPrefs.save();
- }
-
- _handleSaveWatchedProjects() {
- this.$.watchedProjectsEditor.save();
- }
-
- _computeHeaderClass(changed?: boolean) {
+ private computeHeaderClass(changed?: boolean) {
return changed ? 'edited' : '';
}
- _handleSaveEmails() {
- this.$.emailEditor.save();
- }
-
- _handleNewEmailKeydown(e: KeyboardEvent) {
+ // private but used in test
+ handleNewEmailKeydown(e: KeyboardEvent) {
if (e.keyCode === 13) {
// Enter
e.stopPropagation();
- this._handleAddEmailButton();
+ this.handleAddEmailButton();
}
}
- _isNewEmailValid(newEmail?: string): newEmail is string {
+ // private but used in test
+ isNewEmailValid(newEmail?: string): newEmail is string {
return !!newEmail && newEmail.includes('@');
}
- _computeAddEmailButtonEnabled(newEmail?: string, addingEmail?: boolean) {
- return this._isNewEmailValid(newEmail) && !addingEmail;
+ // private but used in test
+ computeAddEmailButtonEnabled() {
+ return this.isNewEmailValid(this.newEmail) && !this.addingEmail;
}
- _handleAddEmailButton() {
- if (!this._isNewEmailValid(this._newEmail)) return;
+ // private but used in test
+ handleAddEmailButton() {
+ if (!this.isNewEmailValid(this.newEmail)) return;
- this._addingEmail = true;
- this.restApiService.addAccountEmail(this._newEmail).then(response => {
- this._addingEmail = false;
+ this.addingEmail = true;
+ this.restApiService.addAccountEmail(this.newEmail).then(response => {
+ this.addingEmail = false;
// If it was unsuccessful.
if (response.status < 200 || response.status >= 300) {
return;
}
- this._lastSentVerificationEmail = this._newEmail;
- this._newEmail = '';
+ this.lastSentVerificationEmail = this.newEmail;
+ this.newEmail = '';
});
}
- _handleShowNumberChanged(e: ValueChangedEvent<boolean>) {
- this._showNumber = e.detail.value;
- }
-
- _handleDisplayedColumnsChanged(e: ValueChangedEvent<string[]>) {
- this._localChangeTableColumns = e.detail.value;
- }
-
- _handleHasEmailsChanged(e: ValueChangedEvent<boolean>) {
- this._emailsChanged = e.detail.value;
- }
-
- _handleHasProjectsChanged(e: ValueChangedEvent<boolean>) {
- this._watchedProjectsChanged = e.detail.value;
- }
-
- _getFilterDocsLink(docsBaseUrl?: string | null) {
+ // private but used in test
+ getFilterDocsLink(docsBaseUrl?: string | null) {
let base = docsBaseUrl;
if (!base || !ABSOLUTE_URL_PATTERN.test(base)) {
base = GERRIT_DOCS_BASE_URL;
@@ -502,8 +1092,8 @@
return base + GERRIT_DOCS_FILTER_PATH;
}
- _handleToggleDark() {
- if (this._isDark) {
+ private handleToggleDark() {
+ if (this.isDark) {
window.localStorage.removeItem('dark-theme');
} else {
window.localStorage.setItem('dark-theme', 'true');
@@ -511,14 +1101,16 @@
this.reloadPage();
}
+ // private but used in test
reloadPage() {
windowLocationReload();
}
- _showHttpAuth(config?: ServerInfo) {
- if (config && config.auth && config.auth.git_basic_auth_policy) {
+ // private but used in test
+ showHttpAuth() {
+ if (this.serverConfig?.auth?.git_basic_auth_policy) {
return HTTP_AUTH.includes(
- config.auth.git_basic_auth_policy.toUpperCase()
+ this.serverConfig.auth.git_basic_auth_policy.toUpperCase()
);
}
@@ -528,57 +1120,17 @@
/**
* Work around a issue on iOS when clicking turns into double tap
*/
- _onTapDarkToggle(e: Event) {
+ private onTapDarkToggle(e: Event) {
e.preventDefault();
}
- _handleChangesPerPage() {
- this.set(
- '_localPrefs.changes_per_page',
- Number(this.$.changesPerPageSelect.value)
- );
- }
-
- _handleDateFormat() {
- this.set('_localPrefs.date_format', this.$.dateTimeFormatSelect.value);
- }
-
- _handleTimeFormat() {
- this.set('_localPrefs.time_format', this.$.timeFormatSelect.value);
- }
-
- _handleEmailStrategy() {
- this.set(
- '_localPrefs.email_strategy',
- this.$.emailNotificationsSelect.value
- );
- }
-
- _handleEmailFormat() {
- this.set('_localPrefs.email_format', this.$.emailFormatSelect.value);
- }
-
- _handleDefaultBaseForMerges() {
- this.set(
- '_localPrefs.default_base_for_merges',
- this.$.defaultBaseForMergesSelect.value
- );
- }
-
- _handleDiffView() {
- this.set(
- '_localPrefs.diff_view',
- this.$.diffViewSelect.value as DiffViewMode
- );
- }
-
/**
* bind-value has type string so we have to convert anything inputed
* to string.
*
* This is so typescript template checker doesn't fail.
*/
- _convertToString(
+ private convertToString(
key?:
| DateFormat
| DefaultBase
@@ -590,10 +1142,6 @@
) {
return key !== undefined ? String(key) : '';
}
-
- _handleHasUnsavedChangesChanged(e: ValueChangedEvent<boolean>) {
- this._diffPrefsChanged = e.detail.value;
- }
}
declare global {
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.ts b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.ts
deleted file mode 100644
index 00f85d8..0000000
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.ts
+++ /dev/null
@@ -1,590 +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`
- <style include="gr-font-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-paper-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="shared-styles">
- :host {
- color: var(--primary-text-color);
- }
- h2 {
- font-family: var(--header-font-family);
- font-size: var(--font-size-h2);
- font-weight: var(--font-weight-h2);
- line-height: var(--line-height-h2);
- }
- .newEmailInput {
- width: 20em;
- }
- #email {
- margin-bottom: var(--spacing-l);
- }
- .main section.darkToggle {
- display: block;
- }
- .filters p,
- .darkToggle p {
- margin-bottom: var(--spacing-l);
- }
- .queryExample em {
- color: violet;
- }
- .toggle {
- align-items: center;
- display: flex;
- margin-bottom: var(--spacing-l);
- margin-right: var(--spacing-l);
- }
- </style>
- <style include="gr-form-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-menu-page-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-page-nav-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <div class="loading" hidden$="[[!_loading]]">Loading...</div>
- <div hidden$="[[_loading]]" hidden="">
- <gr-page-nav class="navStyles">
- <ul>
- <li><a href="#Profile">Profile</a></li>
- <li><a href="#Preferences">Preferences</a></li>
- <li><a href="#DiffPreferences">Diff Preferences</a></li>
- <li><a href="#EditPreferences">Edit Preferences</a></li>
- <li><a href="#Menu">Menu</a></li>
- <li><a href="#ChangeTableColumns">Change Table Columns</a></li>
- <li><a href="#Notifications">Notifications</a></li>
- <li><a href="#EmailAddresses">Email Addresses</a></li>
- <template is="dom-if" if="[[_showHttpAuth(_serverConfig)]]">
- <li><a href="#HTTPCredentials">HTTP Credentials</a></li>
- </template>
- <li hidden$="[[!_serverConfig.sshd]]">
- <a href="#SSHKeys"> SSH Keys </a>
- </li>
- <li hidden$="[[!_serverConfig.receive.enable_signed_push]]">
- <a href="#GPGKeys"> GPG Keys </a>
- </li>
- <li><a href="#Groups">Groups</a></li>
- <li><a href="#Identities">Identities</a></li>
- <template
- is="dom-if"
- if="[[_serverConfig.auth.use_contributor_agreements]]"
- >
- <li>
- <a href="#Agreements">Agreements</a>
- </li>
- </template>
- <li><a href="#MailFilters">Mail Filters</a></li>
- <gr-endpoint-decorator name="settings-menu-item">
- </gr-endpoint-decorator>
- </ul>
- </gr-page-nav>
- <div class="main gr-form-styles">
- <h1 class="heading-1">User Settings</h1>
- <h2 id="Theme">Theme</h2>
- <section class="darkToggle">
- <div class="toggle">
- <paper-toggle-button
- aria-labelledby="darkThemeToggleLabel"
- checked="[[_isDark]]"
- on-change="_handleToggleDark"
- on-click="_onTapDarkToggle"
- ></paper-toggle-button>
- <div id="darkThemeToggleLabel">
- Dark theme (the toggle reloads the page)
- </div>
- </div>
- </section>
- <h2 id="Profile" class$="[[_computeHeaderClass(_accountInfoChanged)]]">
- Profile
- </h2>
- <fieldset id="profile">
- <gr-account-info
- id="accountInfo"
- has-unsaved-changes="{{_accountInfoChanged}}"
- ></gr-account-info>
- <gr-button
- on-click="_handleSaveAccountInfo"
- disabled="[[!_accountInfoChanged]]"
- >Save changes</gr-button
- >
- </fieldset>
- <h2 id="Preferences" class$="[[_computeHeaderClass(_prefsChanged)]]">
- Preferences
- </h2>
- <fieldset id="preferences">
- <section>
- <label class="title" for="changesPerPageSelect"
- >Changes per page</label
- >
- <span class="value">
- <gr-select
- bind-value="[[_convertToString(_localPrefs.changes_per_page)]]"
- on-change="_handleChangesPerPage"
- >
- <select id="changesPerPageSelect">
- <option value="10">10 rows per page</option>
- <option value="25">25 rows per page</option>
- <option value="50">50 rows per page</option>
- <option value="100">100 rows per page</option>
- </select>
- </gr-select>
- </span>
- </section>
- <section>
- <label class="title" for="dateTimeFormatSelect"
- >Date/time format</label
- >
- <span class="value">
- <gr-select
- bind-value="[[_convertToString(_localPrefs.date_format)]]"
- on-change="_handleDateFormat"
- >
- <select id="dateTimeFormatSelect">
- <option value="STD">Jun 3 ; Jun 3, 2016</option>
- <option value="US">06/03 ; 06/03/16</option>
- <option value="ISO">06-03 ; 2016-06-03</option>
- <option value="EURO">3. Jun ; 03.06.2016</option>
- <option value="UK">03/06 ; 03/06/2016</option>
- </select>
- </gr-select>
- <gr-select
- bind-value="[[_convertToString(_localPrefs.time_format)]]"
- aria-label="Time Format"
- on-change="_handleTimeFormat"
- >
- <select id="timeFormatSelect">
- <option value="HHMM_12">4:10 PM</option>
- <option value="HHMM_24">16:10</option>
- </select>
- </gr-select>
- </span>
- </section>
- <section>
- <label class="title" for="emailNotificationsSelect"
- >Email notifications</label
- >
- <span class="value">
- <gr-select
- bind-value="[[_convertToString(_localPrefs.email_strategy)]]"
- on-change="_handleEmailStrategy"
- >
- <select id="emailNotificationsSelect">
- <option value="CC_ON_OWN_COMMENTS">Every comment</option>
- <option value="ENABLED">Only comments left by others</option>
- <option value="ATTENTION_SET_ONLY">
- Only when I am in the attention set
- </option>
- <option value="DISABLED">None</option>
- </select>
- </gr-select>
- </span>
- </section>
- <section hidden$="[[!_convertToString(_localPrefs.email_format)]]">
- <label class="title" for="emailFormatSelect">Email format</label>
- <span class="value">
- <gr-select
- bind-value="[[_convertToString(_localPrefs.email_format)]]"
- on-change="_handleEmailFormat"
- >
- <select id="emailFormatSelect">
- <option value="HTML_PLAINTEXT">HTML and plaintext</option>
- <option value="PLAINTEXT">Plaintext only</option>
- </select>
- </gr-select>
- </span>
- </section>
- <section hidden$="[[!_localPrefs.default_base_for_merges]]">
- <span class="title">Default Base For Merges</span>
- <span class="value">
- <gr-select
- bind-value="[[_convertToString(_localPrefs.default_base_for_merges)]]"
- on-change="_handleDefaultBaseForMerges"
- >
- <select id="defaultBaseForMergesSelect">
- <option value="AUTO_MERGE">Auto Merge</option>
- <option value="FIRST_PARENT">First Parent</option>
- </select>
- </gr-select>
- </span>
- </section>
- <section>
- <label class="title" for="relativeDateInChangeTable"
- >Show Relative Dates In Changes Table</label
- >
- <span class="value">
- <input
- id="relativeDateInChangeTable"
- type="checkbox"
- checked$="[[_localPrefs.relative_date_in_change_table]]"
- on-change="_handleRelativeDateInChangeTable"
- />
- </span>
- </section>
- <section>
- <span class="title">Diff view</span>
- <span class="value">
- <gr-select
- bind-value="[[_convertToString(_localPrefs.diff_view)]]"
- on-change="_handleDiffView"
- >
- <select id="diffViewSelect">
- <option value="SIDE_BY_SIDE">Side by side</option>
- <option value="UNIFIED_DIFF">Unified diff</option>
- </select>
- </gr-select>
- </span>
- </section>
- <section>
- <label for="showSizeBarsInFileList" class="title"
- >Show size bars in file list</label
- >
- <span class="value">
- <input
- id="showSizeBarsInFileList"
- type="checkbox"
- checked$="[[_localPrefs.size_bar_in_change_table]]"
- on-change="_handleShowSizeBarsInFileListChanged"
- />
- </span>
- </section>
- <section>
- <label for="publishCommentsOnPush" class="title"
- >Publish comments on push</label
- >
- <span class="value">
- <input
- id="publishCommentsOnPush"
- type="checkbox"
- checked$="[[_localPrefs.publish_comments_on_push]]"
- on-change="_handlePublishCommentsOnPushChanged"
- />
- </span>
- </section>
- <section>
- <label for="workInProgressByDefault" class="title"
- >Set new changes to "work in progress" by default</label
- >
- <span class="value">
- <input
- id="workInProgressByDefault"
- type="checkbox"
- checked$="[[_localPrefs.work_in_progress_by_default]]"
- on-change="_handleWorkInProgressByDefault"
- />
- </span>
- </section>
- <section>
- <label for="disableKeyboardShortcuts" class="title"
- >Disable all keyboard shortcuts</label
- >
- <span class="value">
- <input
- id="disableKeyboardShortcuts"
- type="checkbox"
- checked$="[[_localPrefs.disable_keyboard_shortcuts]]"
- on-change="_handleDisableKeyboardShortcutsChanged"
- />
- </span>
- </section>
- <section>
- <label for="disableTokenHighlighting" class="title"
- >Disable token highlighting on hover</label
- >
- <span class="value">
- <input
- id="disableTokenHighlighting"
- type="checkbox"
- checked$="[[_localPrefs.disable_token_highlighting]]"
- on-change="_handleDisableTokenHighlightingChanged"
- />
- </span>
- </section>
- <section>
- <label for="insertSignedOff" class="title">
- Insert Signed-off-by Footer For Inline Edit Changes
- </label>
- <span class="value">
- <input
- id="insertSignedOff"
- type="checkbox"
- checked$="[[_localPrefs.signed_off_by]]"
- on-change="_handleInsertSignedOff"
- />
- </span>
- </section>
- <gr-button
- id="savePrefs"
- on-click="_handleSavePreferences"
- disabled="[[!_prefsChanged]]"
- >Save changes</gr-button
- >
- </fieldset>
- <h2
- id="DiffPreferences"
- class$="[[_computeHeaderClass(_diffPrefsChanged)]]"
- >
- Diff Preferences
- </h2>
- <fieldset id="diffPreferences">
- <gr-diff-preferences
- id="diffPrefs"
- on-has-unsaved-changes-changed="_handleHasUnsavedChangesChanged"
- ></gr-diff-preferences>
- <gr-button
- id="saveDiffPrefs"
- on-click="_handleSaveDiffPreferences"
- disabled$="[[!_diffPrefsChanged]]"
- >Save changes</gr-button
- >
- </fieldset>
- <gr-edit-preferences id="editPrefs"></gr-edit-preferences>
- <gr-menu-editor></gr-menu-editor>
- <h2
- id="ChangeTableColumns"
- class$="[[_computeHeaderClass(_changeTableChanged)]]"
- >
- Change Table Columns
- </h2>
- <fieldset id="changeTableColumns">
- <gr-change-table-editor
- show-number="[[_showNumber]]"
- on-show-number-changed="_handleShowNumberChanged"
- server-config="[[_serverConfig]]"
- displayed-columns="[[_localChangeTableColumns]]"
- on-displayed-columns-changed="_handleDisplayedColumnsChanged"
- >
- </gr-change-table-editor>
- <gr-button
- id="saveChangeTable"
- on-click="_handleSaveChangeTable"
- disabled="[[!_changeTableChanged]]"
- >Save changes</gr-button
- >
- </fieldset>
- <h2
- id="Notifications"
- class$="[[_computeHeaderClass(_watchedProjectsChanged)]]"
- >
- Notifications
- </h2>
- <fieldset id="watchedProjects">
- <gr-watched-projects-editor
- has-unsaved-changes="[[_watchedProjectsChanged]]"
- on-has-unsaved-changes-changed="_handleHasProjectsChanged"
- id="watchedProjectsEditor"
- ></gr-watched-projects-editor>
- <gr-button
- on-click="_handleSaveWatchedProjects"
- disabled$="[[!_watchedProjectsChanged]]"
- id="_handleSaveWatchedProjects"
- >Save changes</gr-button
- >
- </fieldset>
- <h2 id="EmailAddresses" class$="[[_computeHeaderClass(_emailsChanged)]]">
- Email Addresses
- </h2>
- <fieldset id="email">
- <gr-email-editor
- id="emailEditor"
- has-unsaved-changes="[[_emailsChanged]]"
- on-has-unsaved-changes-changed="_handleHasEmailsChanged"
- ></gr-email-editor>
- <gr-button on-click="_handleSaveEmails" disabled$="[[!_emailsChanged]]"
- >Save changes</gr-button
- >
- </fieldset>
- <fieldset id="newEmail">
- <section>
- <span class="title">New email address</span>
- <span class="value">
- <iron-input
- class="newEmailInput"
- bind-value="{{_newEmail}}"
- type="text"
- on-keydown="_handleNewEmailKeydown"
- placeholder="email@example.com"
- >
- <input
- class="newEmailInput"
- type="text"
- disabled="[[_addingEmail]]"
- on-keydown="_handleNewEmailKeydown"
- placeholder="email@example.com"
- />
- </iron-input>
- </span>
- </section>
- <section
- id="verificationSentMessage"
- hidden$="[[!_lastSentVerificationEmail]]"
- >
- <p>
- A verification email was sent to
- <em>[[_lastSentVerificationEmail]]</em>. Please check your inbox.
- </p>
- </section>
- <gr-button
- disabled="[[!_computeAddEmailButtonEnabled(_newEmail, _addingEmail)]]"
- on-click="_handleAddEmailButton"
- >Send verification</gr-button
- >
- </fieldset>
- <template is="dom-if" if="[[_showHttpAuth(_serverConfig)]]">
- <div>
- <h2 id="HTTPCredentials">HTTP Credentials</h2>
- <fieldset>
- <gr-http-password id="httpPass"></gr-http-password>
- </fieldset>
- </div>
- </template>
- <div hidden$="[[!_serverConfig.sshd]]">
- <h2 id="SSHKeys" class$="[[_computeHeaderClass(_keysChanged)]]">
- SSH keys
- </h2>
- <gr-ssh-editor
- id="sshEditor"
- has-unsaved-changes-changed="handleUnsavedChangesChanged"
- ></gr-ssh-editor>
- </div>
- <div hidden$="[[!_serverConfig.receive.enable_signed_push]]">
- <h2 id="GPGKeys" class$="[[_computeHeaderClass(_gpgKeysChanged)]]">
- GPG keys
- </h2>
- <gr-gpg-editor
- id="gpgEditor"
- has-unsaved-changes="[[_gpgKeysChanged]]"
- on-has-unsaved-changes-changed="_handleGpgEditorHasSavedChanges"
- ></gr-gpg-editor>
- </div>
- <h2 id="Groups">Groups</h2>
- <fieldset>
- <gr-group-list id="groupList"></gr-group-list>
- </fieldset>
- <h2 id="Identities">Identities</h2>
- <fieldset>
- <gr-identities
- id="identities"
- server-config="[[_serverConfig]]"
- ></gr-identities>
- </fieldset>
- <template
- is="dom-if"
- if="[[_serverConfig.auth.use_contributor_agreements]]"
- >
- <h2 id="Agreements">Agreements</h2>
- <fieldset>
- <gr-agreements-list id="agreementsList"></gr-agreements-list>
- </fieldset>
- </template>
- <h2 id="MailFilters">Mail Filters</h2>
- <fieldset class="filters">
- <p>
- Gerrit emails include metadata about the change to support writing
- mail filters.
- </p>
- <p>
- Here are some example Gmail queries that can be used for filters or
- for searching through archived messages. View the
- <a
- href$="[[_getFilterDocsLink(_docsBaseUrl)]]"
- target="_blank"
- rel="nofollow"
- >Gerrit documentation</a
- >
- for the complete set of footers.
- </p>
- <table>
- <tbody>
- <tr>
- <th>Name</th>
- <th>Query</th>
- </tr>
- <tr>
- <td>Changes requesting my review</td>
- <td>
- <code class="queryExample">
- "Gerrit-Reviewer: <em>Your Name</em>
- <<em>your.email@example.com</em>>"
- </code>
- </td>
- </tr>
- <tr>
- <td>Changes requesting my attention</td>
- <td>
- <code class="queryExample">
- "Gerrit-Attention: <em>Your Name</em>
- <<em>your.email@example.com</em>>"
- </code>
- </td>
- </tr>
- <tr>
- <td>Changes from a specific owner</td>
- <td>
- <code class="queryExample">
- "Gerrit-Owner: <em>Owner name</em>
- <<em>owner.email@example.com</em>>"
- </code>
- </td>
- </tr>
- <tr>
- <td>Changes targeting a specific branch</td>
- <td>
- <code class="queryExample">
- "Gerrit-Branch: <em>branch-name</em>"
- </code>
- </td>
- </tr>
- <tr>
- <td>Changes in a specific project</td>
- <td>
- <code class="queryExample">
- "Gerrit-Project: <em>project-name</em>"
- </code>
- </td>
- </tr>
- <tr>
- <td>Messages related to a specific Change ID</td>
- <td>
- <code class="queryExample">
- "Gerrit-Change-Id: <em>Change ID</em>"
- </code>
- </td>
- </tr>
- <tr>
- <td>Messages related to a specific change number</td>
- <td>
- <code class="queryExample">
- "Gerrit-Change-Number: <em>change number</em>"
- </code>
- </td>
- </tr>
- </tbody>
- </table>
- </fieldset>
- <gr-endpoint-decorator name="settings-screen"> </gr-endpoint-decorator>
- </div>
- </div>
-`;
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts
index 7f81f42..a514f00 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts
@@ -19,7 +19,7 @@
import './gr-settings-view';
import {GrSettingsView} from './gr-settings-view';
import {GerritView} from '../../../services/router/router-model';
-import {queryAndAssert, stubRestApi} from '../../../test/test-utils';
+import {queryAll, queryAndAssert, stubRestApi} from '../../../test/test-utils';
import {
AuthInfo,
AccountDetailInfo,
@@ -56,7 +56,7 @@
let config: ServerInfo;
function valueOf(title: string, id: string) {
- const sections = element.root?.querySelectorAll(`#${id} section`) ?? [];
+ const sections = queryAll(element, `#${id} section`);
let titleEl;
for (let i = 0; i < sections.length; i++) {
titleEl = sections[i].querySelector('.title');
@@ -122,10 +122,420 @@
stubRestApi('getAccountEmails').returns(Promise.resolve(undefined));
stubRestApi('getConfig').returns(Promise.resolve(config));
element = basicFixture.instantiate();
+ await element.updateComplete;
// Allow the element to render.
if (element._testOnly_loadingPromise)
await element._testOnly_loadingPromise;
+ await element.updateComplete;
+ });
+
+ test('renders', async () => {
+ sinon
+ .stub(element, 'getFilterDocsLink')
+ .returns('https://test.com/user-notify.html');
+ element.docsBaseUrl = 'https://test.com';
+ await element.updateComplete;
+ // this cannot be formatted with /* HTML */, because it breaks test
+ expect(element).shadowDom.to.equal(/* HTML*/ `<div
+ class="loading"
+ hidden=""
+ >
+ Loading...
+ </div>
+ <div>
+ <gr-page-nav class="navStyles">
+ <ul>
+ <li><a href="#Profile"> Profile </a></li>
+ <li><a href="#Preferences"> Preferences </a></li>
+ <li><a href="#DiffPreferences"> Diff Preferences </a></li>
+ <li><a href="#EditPreferences"> Edit Preferences </a></li>
+ <li><a href="#Menu"> Menu </a></li>
+ <li><a href="#ChangeTableColumns"> Change Table Columns </a></li>
+ <li><a href="#Notifications"> Notifications </a></li>
+ <li><a href="#EmailAddresses"> Email Addresses </a></li>
+ <li><a href="#Groups"> Groups </a></li>
+ <li><a href="#Identities"> Identities </a></li>
+ <li><a href="#MailFilters"> Mail Filters </a></li>
+ <gr-endpoint-decorator name="settings-menu-item">
+ </gr-endpoint-decorator>
+ </ul>
+ </gr-page-nav>
+ <div class="gr-form-styles main">
+ <h1 class="heading-1">User Settings</h1>
+ <h2 id="Theme">Theme</h2>
+ <section class="darkToggle">
+ <div class="toggle">
+ <paper-toggle-button
+ aria-disabled="false"
+ aria-labelledby="darkThemeToggleLabel"
+ aria-pressed="false"
+ role="button"
+ style="touch-action: none;"
+ tabindex="0"
+ toggles=""
+ >
+ </paper-toggle-button>
+ <div id="darkThemeToggleLabel">
+ Dark theme (the toggle reloads the page)
+ </div>
+ </div>
+ </section>
+ <h2 id="Profile">Profile</h2>
+ <fieldset id="profile">
+ <gr-account-info id="accountInfo"> </gr-account-info>
+ <gr-button
+ aria-disabled="true"
+ disabled=""
+ role="button"
+ tabindex="-1"
+ >
+ Save changes
+ </gr-button>
+ </fieldset>
+ <h2 id="Preferences">Preferences</h2>
+ <fieldset id="preferences">
+ <section>
+ <label class="title" for="changesPerPageSelect">
+ Changes per page
+ </label>
+ <span class="value">
+ <gr-select>
+ <select id="changesPerPageSelect">
+ <option value="10">10 rows per page</option>
+ <option value="25">25 rows per page</option>
+ <option value="50">50 rows per page</option>
+ <option value="100">100 rows per page</option>
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ <section>
+ <label class="title" for="dateTimeFormatSelect">
+ Date/time format
+ </label>
+ <span class="value">
+ <gr-select>
+ <select id="dateTimeFormatSelect">
+ <option value="STD">Jun 3 ; Jun 3, 2016</option>
+ <option value="US">06/03 ; 06/03/16</option>
+ <option value="ISO">06-03 ; 2016-06-03</option>
+ <option value="EURO">3. Jun ; 03.06.2016</option>
+ <option value="UK">03/06 ; 03/06/2016</option>
+ </select>
+ </gr-select>
+ <gr-select aria-label="Time Format">
+ <select id="timeFormatSelect">
+ <option value="HHMM_12">4:10 PM</option>
+ <option value="HHMM_24">16:10</option>
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ <section>
+ <label class="title" for="emailNotificationsSelect">
+ Email notifications
+ </label>
+ <span class="value">
+ <gr-select>
+ <select id="emailNotificationsSelect">
+ <option value="CC_ON_OWN_COMMENTS">Every comment</option>
+ <option value="ENABLED">
+ Only comments left by others
+ </option>
+ <option value="ATTENTION_SET_ONLY">
+ Only when I am in the attention set
+ </option>
+ <option value="DISABLED">None</option>
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ <section>
+ <label class="title" for="emailFormatSelect">
+ Email format
+ </label>
+ <span class="value">
+ <gr-select>
+ <select id="emailFormatSelect">
+ <option value="HTML_PLAINTEXT">HTML and plaintext</option>
+ <option value="PLAINTEXT">Plaintext only</option>
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ <section>
+ <span class="title"> Default Base For Merges </span>
+ <span class="value">
+ <gr-select>
+ <select id="defaultBaseForMergesSelect">
+ <option value="AUTO_MERGE">Auto Merge</option>
+ <option value="FIRST_PARENT">First Parent</option>
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ <section>
+ <label class="title" for="relativeDateInChangeTable">
+ Show Relative Dates In Changes Table
+ </label>
+ <span class="value">
+ <input id="relativeDateInChangeTable" type="checkbox" />
+ </span>
+ </section>
+ <section>
+ <span class="title"> Diff view </span>
+ <span class="value">
+ <gr-select>
+ <select id="diffViewSelect">
+ <option value="SIDE_BY_SIDE">Side by side</option>
+ <option value="UNIFIED_DIFF">Unified diff</option>
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ <section>
+ <label class="title" for="showSizeBarsInFileList">
+ Show size bars in file list
+ </label>
+ <span class="value">
+ <input
+ checked=""
+ id="showSizeBarsInFileList"
+ type="checkbox"
+ />
+ </span>
+ </section>
+ <section>
+ <label class="title" for="publishCommentsOnPush">
+ Publish comments on push
+ </label>
+ <span class="value">
+ <input id="publishCommentsOnPush" type="checkbox" />
+ </span>
+ </section>
+ <section>
+ <label class="title" for="workInProgressByDefault">
+ Set new changes to "work in progress" by default
+ </label>
+ <span class="value">
+ <input id="workInProgressByDefault" type="checkbox" />
+ </span>
+ </section>
+ <section>
+ <label class="title" for="disableKeyboardShortcuts">
+ Disable all keyboard shortcuts
+ </label>
+ <span class="value">
+ <input id="disableKeyboardShortcuts" type="checkbox" />
+ </span>
+ </section>
+ <section>
+ <label class="title" for="disableTokenHighlighting">
+ Disable token highlighting on hover
+ </label>
+ <span class="value">
+ <input id="disableTokenHighlighting" type="checkbox" />
+ </span>
+ </section>
+ <section>
+ <label class="title" for="insertSignedOff">
+ Insert Signed-off-by Footer For Inline Edit Changes
+ </label>
+ <span class="value">
+ <input id="insertSignedOff" type="checkbox" />
+ </span>
+ </section>
+ <gr-button
+ aria-disabled="true"
+ disabled=""
+ id="savePrefs"
+ role="button"
+ tabindex="-1"
+ >
+ Save changes
+ </gr-button>
+ </fieldset>
+ <h2 id="DiffPreferences">Diff Preferences</h2>
+ <fieldset id="diffPreferences">
+ <gr-diff-preferences id="diffPrefs"> </gr-diff-preferences>
+ <gr-button
+ aria-disabled="true"
+ disabled=""
+ id="saveDiffPrefs"
+ role="button"
+ tabindex="-1"
+ >
+ Save changes
+ </gr-button>
+ </fieldset>
+ <gr-edit-preferences id="editPrefs"> </gr-edit-preferences>
+ <gr-menu-editor> </gr-menu-editor>
+ <h2 id="ChangeTableColumns">Change Table Columns</h2>
+ <fieldset id="changeTableColumns">
+ <gr-change-table-editor> </gr-change-table-editor>
+ <gr-button
+ aria-disabled="true"
+ disabled=""
+ id="saveChangeTable"
+ role="button"
+ tabindex="-1"
+ >
+ Save changes
+ </gr-button>
+ </fieldset>
+ <h2 id="Notifications">Notifications</h2>
+ <fieldset id="watchedProjects">
+ <gr-watched-projects-editor id="watchedProjectsEditor">
+ </gr-watched-projects-editor>
+ <gr-button
+ aria-disabled="true"
+ disabled=""
+ id="_handleSaveWatchedProjects"
+ role="button"
+ tabindex="-1"
+ >
+ Save changes
+ </gr-button>
+ </fieldset>
+ <h2 id="EmailAddresses">Email Addresses</h2>
+ <fieldset id="email">
+ <gr-email-editor id="emailEditor"> </gr-email-editor>
+ <gr-button
+ aria-disabled="true"
+ disabled=""
+ role="button"
+ tabindex="-1"
+ >
+ Save changes
+ </gr-button>
+ </fieldset>
+ <fieldset id="newEmail">
+ <section>
+ <span class="title"> New email address </span>
+ <span class="value">
+ <iron-input class="newEmailInput">
+ <input
+ class="newEmailInput"
+ placeholder="email@example.com"
+ type="text"
+ />
+ </iron-input>
+ </span>
+ </section>
+ <section hidden="" id="verificationSentMessage">
+ <p>
+ A verification email was sent to <em>
+ </em>
+ . Please check your
+ inbox.
+ </p>
+ </section>
+ <gr-button
+ aria-disabled="true"
+ disabled=""
+ role="button"
+ tabindex="-1"
+ >
+ Send verification
+ </gr-button>
+ </fieldset>
+ <h2 id="Groups">Groups</h2>
+ <fieldset><gr-group-list id="groupList"> </gr-group-list></fieldset>
+ <h2 id="Identities">Identities</h2>
+ <fieldset>
+ <gr-identities id="identities"> </gr-identities>
+ </fieldset>
+ <h2 id="MailFilters">Mail Filters</h2>
+ <fieldset class="filters">
+ <p>
+ Gerrit emails include metadata about the change to support writing
+ mail filters.
+ </p>
+ <p>
+ Here are some example Gmail queries that can be used for filters
+ or for searching through archived messages. View the
+ <a
+ href="https://test.com/user-notify.html"
+ rel="nofollow"
+ target="_blank"
+ >
+ Gerrit documentation
+ </a>
+ for the complete set of footers.
+ </p>
+ <table>
+ <tbody>
+ <tr>
+ <th>Name</th>
+ <th>Query</th>
+ </tr>
+ <tr>
+ <td>Changes requesting my review</td>
+ <td>
+ <code class="queryExample">
+ "Gerrit-Reviewer: <em> Your Name </em> <
+ <em> your.email@example.com </em> >"
+ </code>
+ </td>
+ </tr>
+ <tr>
+ <td>Changes requesting my attention</td>
+ <td>
+ <code class="queryExample">
+ "Gerrit-Attention: <em> Your Name </em> <
+ <em> your.email@example.com </em> >"
+ </code>
+ </td>
+ </tr>
+ <tr>
+ <td>Changes from a specific owner</td>
+ <td>
+ <code class="queryExample">
+ "Gerrit-Owner: <em> Owner name </em> <
+ <em> owner.email@example.com </em> >"
+ </code>
+ </td>
+ </tr>
+ <tr>
+ <td>Changes targeting a specific branch</td>
+ <td>
+ <code class="queryExample">
+ "Gerrit-Branch: <em> branch-name </em> "
+ </code>
+ </td>
+ </tr>
+ <tr>
+ <td>Changes in a specific project</td>
+ <td>
+ <code class="queryExample">
+ "Gerrit-Project: <em> project-name </em> "
+ </code>
+ </td>
+ </tr>
+ <tr>
+ <td>Messages related to a specific Change ID</td>
+ <td>
+ <code class="queryExample">
+ "Gerrit-Change-Id: <em> Change ID </em> "
+ </code>
+ </td>
+ </tr>
+ <tr>
+ <td>Messages related to a specific change number</td>
+ <td>
+ <code class="queryExample">
+ "Gerrit-Change-Number: <em> change number </em> "
+ </code>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </fieldset>
+ <gr-endpoint-decorator name="settings-screen">
+ </gr-endpoint-decorator>
+ </div>
+ </div>`);
});
test('theme changing', async () => {
@@ -141,7 +551,7 @@
assert.isTrue(window.localStorage.getItem('dark-theme') === 'true');
assert.isTrue(reloadStub.calledOnce);
- element._isDark = true;
+ element.isDark = true;
await flush();
MockInteractions.tap(themeToggle);
assert.isFalse(window.localStorage.getItem('dark-theme') === 'true');
@@ -258,14 +668,14 @@
false
);
- assert.isFalse(element._prefsChanged);
+ assert.isFalse(element.prefsChanged);
const publishOnPush = valueOf('Publish comments on push', 'preferences')!
.firstElementChild!;
MockInteractions.tap(publishOnPush);
- assert.isTrue(element._prefsChanged);
+ assert.isTrue(element.prefsChanged);
stubRestApi('savePreferences').callsFake(prefs => {
assertMenusEqual(prefs.my, preferences.my);
@@ -274,8 +684,8 @@
});
// Save the change.
- await element._handleSavePreferences();
- assert.isFalse(element._prefsChanged);
+ await element.handleSavePreferences();
+ assert.isFalse(element.prefsChanged);
});
test('publish comments on push', async () => {
@@ -285,7 +695,7 @@
)!.firstElementChild!;
MockInteractions.tap(publishCommentsOnPush);
- assert.isTrue(element._prefsChanged);
+ assert.isTrue(element.prefsChanged);
stubRestApi('savePreferences').callsFake(prefs => {
assert.equal(prefs.publish_comments_on_push, true);
@@ -293,8 +703,8 @@
});
// Save the change.
- await element._handleSavePreferences();
- assert.isFalse(element._prefsChanged);
+ await element.handleSavePreferences();
+ assert.isFalse(element.prefsChanged);
});
test('set new changes work-in-progress', async () => {
@@ -304,7 +714,7 @@
)!.firstElementChild!;
MockInteractions.tap(newChangesWorkInProgress);
- assert.isTrue(element._prefsChanged);
+ assert.isTrue(element.prefsChanged);
stubRestApi('savePreferences').callsFake(prefs => {
assert.equal(prefs.work_in_progress_by_default, true);
@@ -312,37 +722,40 @@
});
// Save the change.
- await element._handleSavePreferences();
- assert.isFalse(element._prefsChanged);
+ await element.handleSavePreferences();
+ assert.isFalse(element.prefsChanged);
});
- test('add email validation', () => {
- assert.isFalse(element._isNewEmailValid('invalid email'));
- assert.isTrue(element._isNewEmailValid('vaguely@valid.email'));
+ test('add email validation', async () => {
+ assert.isFalse(element.isNewEmailValid('invalid email'));
+ assert.isTrue(element.isNewEmailValid('vaguely@valid.email'));
- assert.isFalse(
- element._computeAddEmailButtonEnabled('invalid email', true)
- );
- assert.isFalse(
- element._computeAddEmailButtonEnabled('vaguely@valid.email', true)
- );
- assert.isTrue(
- element._computeAddEmailButtonEnabled('vaguely@valid.email', false)
- );
+ element.newEmail = 'invalid email';
+ element.addingEmail = true;
+ await element.updateComplete;
+ assert.isFalse(element.computeAddEmailButtonEnabled());
+ element.newEmail = 'vaguely@valid.email';
+ element.addingEmail = true;
+ await element.updateComplete;
+ assert.isFalse(element.computeAddEmailButtonEnabled());
+ element.newEmail = 'vaguely@valid.email';
+ element.addingEmail = false;
+ await element.updateComplete;
+ assert.isTrue(element.computeAddEmailButtonEnabled());
});
test('add email does not save invalid', () => {
const addEmailStub = stubAddAccountEmail(201);
- assert.isFalse(element._addingEmail);
- assert.isNotOk(element._lastSentVerificationEmail);
- element._newEmail = 'invalid email';
+ assert.isFalse(element.addingEmail);
+ assert.isNotOk(element.lastSentVerificationEmail);
+ element.newEmail = 'invalid email';
- element._handleAddEmailButton();
+ element.handleAddEmailButton();
- assert.isFalse(element._addingEmail);
+ assert.isFalse(element.addingEmail);
assert.isFalse(addEmailStub.called);
- assert.isNotOk(element._lastSentVerificationEmail);
+ assert.isNotOk(element.lastSentVerificationEmail);
assert.isFalse(addEmailStub.called);
});
@@ -350,62 +763,59 @@
test('add email does save valid', async () => {
const addEmailStub = stubAddAccountEmail(201);
- assert.isFalse(element._addingEmail);
- assert.isNotOk(element._lastSentVerificationEmail);
- element._newEmail = 'valid@email.com';
+ assert.isFalse(element.addingEmail);
+ assert.isNotOk(element.lastSentVerificationEmail);
+ element.newEmail = 'valid@email.com';
- element._handleAddEmailButton();
+ element.handleAddEmailButton();
- assert.isTrue(element._addingEmail);
+ assert.isTrue(element.addingEmail);
assert.isTrue(addEmailStub.called);
assert.isTrue(addEmailStub.called);
await addEmailStub.lastCall.returnValue;
- assert.isOk(element._lastSentVerificationEmail);
+ assert.isOk(element.lastSentVerificationEmail);
});
test('add email does not set last-email if error', async () => {
const addEmailStub = stubAddAccountEmail(500);
- assert.isNotOk(element._lastSentVerificationEmail);
- element._newEmail = 'valid@email.com';
+ assert.isNotOk(element.lastSentVerificationEmail);
+ element.newEmail = 'valid@email.com';
- element._handleAddEmailButton();
+ element.handleAddEmailButton();
assert.isTrue(addEmailStub.called);
await addEmailStub.lastCall.returnValue;
- assert.isNotOk(element._lastSentVerificationEmail);
+ assert.isNotOk(element.lastSentVerificationEmail);
});
test('emails are loaded without emailToken', () => {
- const emailEditorLoadDataStub = sinon.stub(
- element.$.emailEditor,
- 'loadData'
- );
+ const emailEditorLoadDataStub = sinon.stub(element.emailEditor, 'loadData');
element.params = {
view: GerritView.SETTINGS,
} as AppElementSettingsParam;
- element.connectedCallback();
+ element.firstUpdated();
assert.isTrue(emailEditorLoadDataStub.calledOnce);
});
- test('_handleSaveChangeTable', () => {
+ test('handleSaveChangeTable', () => {
let newColumns = ['Owner', 'Project', 'Branch'];
- element._localChangeTableColumns = newColumns.slice(0);
- element._showNumber = false;
- element._handleSaveChangeTable();
+ element.localChangeTableColumns = newColumns.slice(0);
+ element.showNumber = false;
+ element.handleSaveChangeTable();
assert.deepEqual(element.prefs.change_table, newColumns);
assert.isNotOk(element.prefs.legacycid_in_change_table);
newColumns = ['Size'];
- element._localChangeTableColumns = newColumns;
- element._showNumber = true;
- element._handleSaveChangeTable();
+ element.localChangeTableColumns = newColumns;
+ element.showNumber = true;
+ element.handleSaveChangeTable();
assert.deepEqual(element.prefs.change_table, newColumns);
assert.isTrue(element.prefs.legacycid_in_change_table);
});
- test('_showHttpAuth', () => {
+ test('showHttpAuth', async () => {
const serverConfig: ServerInfo = {
...createServerInfo(),
auth: {
@@ -413,41 +823,48 @@
} as AuthInfo,
};
- assert.isTrue(element._showHttpAuth(serverConfig));
+ element.serverConfig = serverConfig;
+ await element.updateComplete;
+ assert.isTrue(element.showHttpAuth());
- serverConfig.auth.git_basic_auth_policy = 'HTTP_LDAP';
- assert.isTrue(element._showHttpAuth(serverConfig));
+ element.serverConfig.auth.git_basic_auth_policy = 'HTTP_LDAP';
+ await element.updateComplete;
+ assert.isTrue(element.showHttpAuth());
- serverConfig.auth.git_basic_auth_policy = 'LDAP';
- assert.isFalse(element._showHttpAuth(serverConfig));
+ element.serverConfig.auth.git_basic_auth_policy = 'LDAP';
+ await element.updateComplete;
+ assert.isFalse(element.showHttpAuth());
- serverConfig.auth.git_basic_auth_policy = 'OAUTH';
- assert.isFalse(element._showHttpAuth(serverConfig));
+ element.serverConfig.auth.git_basic_auth_policy = 'OAUTH';
+ await element.updateComplete;
+ assert.isFalse(element.showHttpAuth());
- assert.isFalse(element._showHttpAuth(undefined));
+ element.serverConfig = undefined;
+ await element.updateComplete;
+ assert.isFalse(element.showHttpAuth());
});
- suite('_getFilterDocsLink', () => {
+ suite('getFilterDocsLink', () => {
test('with http: docs base URL', () => {
const base = 'http://example.com/';
- const result = element._getFilterDocsLink(base);
+ const result = element.getFilterDocsLink(base);
assert.equal(result, 'http://example.com/user-notify.html');
});
test('with http: docs base URL without slash', () => {
const base = 'http://example.com';
- const result = element._getFilterDocsLink(base);
+ const result = element.getFilterDocsLink(base);
assert.equal(result, 'http://example.com/user-notify.html');
});
test('with https: docs base URL', () => {
const base = 'https://example.com/';
- const result = element._getFilterDocsLink(base);
+ const result = element.getFilterDocsLink(base);
assert.equal(result, 'https://example.com/user-notify.html');
});
test('without docs base URL', () => {
- const result = element._getFilterDocsLink(null);
+ const result = element.getFilterDocsLink(null);
assert.equal(
result,
'https://gerrit-review.googlesource.com/' +
@@ -457,7 +874,7 @@
test('ignores non HTTP links', () => {
const base = 'javascript://alert("evil");';
- const result = element._getFilterDocsLink(base);
+ const result = element.getFilterDocsLink(base);
assert.equal(
result,
'https://gerrit-review.googlesource.com/' +
@@ -474,7 +891,7 @@
let emailEditorLoadDataStub: sinon.SinonStub;
setup(() => {
- emailEditorLoadDataStub = sinon.stub(element.$.emailEditor, 'loadData');
+ emailEditorLoadDataStub = sinon.stub(element.emailEditor, 'loadData');
confirmEmailStub = stubRestApi('confirmEmail').returns(
new Promise(resolve => {
resolveConfirm = resolve;
@@ -482,7 +899,7 @@
);
element.params = {view: GerritView.SETTINGS, emailToken: 'foo'};
- element.connectedCallback();
+ element.firstUpdated();
});
test('it is used to confirm email via rest API', () => {
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 aabdf57..e426e66 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
@@ -32,6 +32,7 @@
import {CoverageRange, DiffLayer} from '../../../types/types';
import {
GrDiffProcessor,
+ GroupConsumer,
KeyLocations,
} from '../gr-diff-processor/gr-diff-processor';
import {
@@ -47,7 +48,6 @@
GrDiffGroupType,
hideInContextControl,
} from '../gr-diff/gr-diff-group';
-import {PolymerSpliceChange} from '@polymer/polymer/interfaces';
import {getLineNumber, getSideByLineEl} from '../gr-diff/gr-diff-utils';
import {fireAlert, fireEvent} from '../../../utils/event-util';
import {afterNextRender} from '@polymer/polymer/lib/utils/render-status';
@@ -58,12 +58,6 @@
const COMMIT_MSG_PATH = '/COMMIT_MSG';
const COMMIT_MSG_LINE_LENGTH = 72;
-export interface GrDiffBuilderElement {
- $: {
- processor: GrDiffProcessor;
- };
-}
-
export function getLineNumberCellWidth(prefs: DiffPreferencesInfo) {
return prefs.font_size * 4;
}
@@ -94,7 +88,10 @@
}
@customElement('gr-diff-builder')
-export class GrDiffBuilderElement extends PolymerElement {
+export class GrDiffBuilderElement
+ extends PolymerElement
+ implements GroupConsumer
+{
static get template() {
return htmlTemplate;
}
@@ -200,6 +197,8 @@
private rangeLayer = new GrRangedCommentLayer();
+ private processor = new GrDiffProcessor();
+
constructor() {
super();
afterNextRender(this, () => {
@@ -212,9 +211,11 @@
}
);
});
+ this.processor.consumer = this;
}
override disconnectedCallback() {
+ this.processor.cancel();
if (this._builder) {
this._builder.clear();
}
@@ -264,8 +265,8 @@
}
this._builder = this._getDiffBuilder();
- this.$.processor.context = this.prefs.context;
- this.$.processor.keyLocations = keyLocations;
+ this.processor.context = this.prefs.context;
+ this.processor.keyLocations = keyLocations;
this._clearDiffContent();
this._builder.addColumns(
@@ -277,7 +278,7 @@
fireEvent(this, 'render-start');
this._cancelableRenderPromise = util.makeCancelable(
- this.$.processor.process(this.diff.content, isBinary).then(() => {
+ this.processor.process(this.diff.content, isBinary).then(() => {
if (this.isImageDiff) {
(this._builder as GrDiffBuilderImage).renderDiff();
}
@@ -414,7 +415,7 @@
}
cancel() {
- this.$.processor.cancel();
+ this.processor.cancel();
if (this._cancelableRenderPromise) {
this._cancelableRenderPromise.cancel();
this._cancelableRenderPromise = null;
@@ -490,28 +491,20 @@
}
/**
- * Forward groups added by the processor to the builder for rendering.
+ * Called when the processor starts converting the diff information from the
+ * server into chunks.
*/
- @observe('_groups.splices')
- _groupsChanged(changeRecord: PolymerSpliceChange<GrDiffGroup[]>) {
- if (!changeRecord || !this._builder) return;
+ clearGroups() {
+ if (!this._builder) return;
+ this._builder.clearGroups();
+ }
- // The processor either removes all groups or adds new ones to the end,
- // so let's simplify the Polymer splices.
- const isRemoval = changeRecord.indexSplices.find(
- splice => splice.removed.length > 0
- );
- if (isRemoval) {
- this._builder.clearGroups();
- return;
- }
- for (const splice of changeRecord.indexSplices) {
- const added = splice.object.slice(
- splice.index,
- splice.index + splice.addedCount
- );
- this._builder.addGroups(added);
- }
+ /**
+ * Called when the processor is done converting a chunk of the diff.
+ */
+ addGroup(group: GrDiffGroup) {
+ if (!this._builder) return;
+ this._builder.addGroups([group]);
fireEvent(this, 'render-progress');
}
@@ -613,7 +606,7 @@
updateRenderPrefs(renderPrefs: RenderPreferences) {
this._builder?.updateRenderPrefs(renderPrefs);
- this.$.processor.updateRenderPrefs(renderPrefs);
+ this.processor.updateRenderPrefs(renderPrefs);
}
}
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
index 581f0fb..bd0e034 100644
--- 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
@@ -20,5 +20,4 @@
<div class="contentWrapper">
<slot></slot>
</div>
- <gr-diff-processor id="processor" groups="{{_groups}}"></gr-diff-processor>
`;
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
index 42d9edf..0ad21b0 100644
--- 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
@@ -15,7 +15,7 @@
* limitations under the License.
*/
import '../../../test/common-test-setup-karma.js';
-import {getMockDiffResponse} from '../../../test/mocks/diff-response.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';
@@ -554,7 +554,7 @@
setup(() => {
element = basicFixture.instantiate();
element.viewMode = 'SIDE_BY_SIDE';
- processStub = sinon.stub(element.$.processor, 'process')
+ processStub = sinon.stub(element.processor, 'process')
.returns(Promise.resolve());
keyLocations = {left: {}, right: {}};
element.prefs = {
@@ -668,7 +668,7 @@
});
test('cancel cancels the processor', () => {
- const processorCancelStub = sinon.stub(element.$.processor, 'cancel');
+ const processorCancelStub = sinon.stub(element.processor, 'cancel');
element.cancel();
assert.isTrue(processorCancelStub.called);
});
@@ -775,7 +775,7 @@
setup(async () => {
element = mockDiffFixture.instantiate();
- diff = getMockDiffResponse();
+ diff = createDiff();
element.diff = diff;
keyLocations = {left: {}, right: {}};
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
index 609b33e..c64f484 100644
--- 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
@@ -20,7 +20,7 @@
import './gr-diff-cursor.js';
import {fixture, html} from '@open-wc/testing-helpers';
import {listenOnce, mockPromise} from '../../../test/test-utils.js';
-import {getMockDiffResponse} from '../../../test/mocks/diff-response.js';
+import {createDiff} from '../../../test/test-data-generators.js';
import {createDefaultDiffPrefs} from '../../../constants/constants.js';
import {GrDiffCursor} from './gr-diff-cursor.js';
@@ -52,7 +52,7 @@
};
diffElement.addEventListener('render', setupDone);
- diff = getMockDiffResponse();
+ diff = createDiff();
diffElement.prefs = createDefaultDiffPrefs();
diffElement.diff = diff;
await promise;
@@ -468,7 +468,7 @@
promise.resolve();
}
diffElement.addEventListener('render', renderHandler);
- diffElement._diffChanged(getMockDiffResponse());
+ diffElement._diffChanged(createDiff());
await promise;
});
@@ -495,7 +495,7 @@
cursor.initialLineNumber = 10;
cursor.side = 'right';
- diffElement._diffChanged(getMockDiffResponse());
+ diffElement._diffChanged(createDiff());
await promise;
});
@@ -548,7 +548,7 @@
end_line: 6,
end_character: 1,
};
- diffElement.$.highlights.selectedRange = {
+ diffElement.highlights.selectedRange = {
side: 'right',
range: someRange,
};
@@ -661,8 +661,8 @@
const diffRenderedPromises =
diffElements.map(diffEl => listenOnce(diffEl, 'render'));
- diffElements[0].diff = getMockDiffResponse();
- diffElements[2].diff = getMockDiffResponse();
+ diffElements[0].diff = createDiff();
+ diffElements[2].diff = createDiff();
await Promise.all([diffRenderedPromises[0], diffRenderedPromises[2]]);
const lastLine = diffElements[0].diff.meta_b.lines;
@@ -683,7 +683,7 @@
assert.equal(cursor.getTargetLineElement().textContent, lastLine);
// Diff 1 finishing to load
- diffElements[1].diff = getMockDiffResponse();
+ diffElements[1].diff = createDiff();
await diffRenderedPromises[1];
// Now we can go down
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight.ts b/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight.ts
index 2ee6c9f..0714645 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight.ts
@@ -1,43 +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 '../../../styles/shared-styles';
import '../gr-selection-action-box/gr-selection-action-box';
-import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-diff-highlight_html';
import {GrAnnotation} from './gr-annotation';
import {normalize} from './gr-range-normalizer';
import {strToClassName} from '../../../utils/dom-util';
-import {customElement, property} from '@polymer/decorators';
import {Side} from '../../../constants/constants';
import {CommentRange} from '../../../types/common';
import {GrSelectionActionBox} from '../gr-selection-action-box/gr-selection-action-box';
-import {GrDiffBuilderElement} from '../gr-diff-builder/gr-diff-builder-element';
import {FILE} from '../gr-diff/gr-diff-line';
import {
getLineElByChild,
getLineNumberByChild,
- getRange,
- getSide,
getSideByLineEl,
GrDiffThreadElement,
} from '../gr-diff/gr-diff-utils';
import {debounce, DelayedTask} from '../../../utils/async-util';
-import {queryAndAssert} from '../../../utils/common-util';
+import {assertIsDefined, queryAndAssert} from '../../../utils/common-util';
interface SidedRange {
side: Side;
@@ -56,51 +38,65 @@
end: NormalizedPosition | null;
}
-@customElement('gr-diff-highlight')
-export class GrDiffHighlight extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
+/**
+ * The methods that we actually want to call on the builder. We don't want a
+ * fully blown dependency on GrDiffBuilderElement.
+ */
+export interface DiffBuilderInterface {
+ getContentTdByLineEl(lineEl?: Element): Element | null;
+}
- @property({type: Array, notify: true})
- commentRanges: SidedRange[] = [];
-
- @property({type: Boolean})
- loggedIn?: boolean;
-
- @property({type: Object})
- _cachedDiffBuilder?: GrDiffBuilderElement;
-
- @property({type: Object, notify: true})
+/**
+ * Handles showing, positioning and interacting with <gr-selection-action-box>.
+ *
+ * Toggles a css class for highlighting comment ranges when the mouse leaves or
+ * enters a comment thread element.
+ */
+export class GrDiffHighlight {
selectedRange?: SidedRange;
+ private diffBuilder?: DiffBuilderInterface;
+
+ private diffTable?: HTMLElement;
+
private selectionChangeTask?: DelayedTask;
- constructor() {
- super();
- this.addEventListener('comment-thread-mouseleave', e =>
- this._handleCommentThreadMouseleave(e)
+ init(diffTable: HTMLElement, diffBuilder: DiffBuilderInterface) {
+ this.cleanup();
+
+ this.diffTable = diffTable;
+ this.diffBuilder = diffBuilder;
+
+ diffTable.addEventListener(
+ 'comment-thread-mouseleave',
+ this.handleCommentThreadMouseleave
);
- this.addEventListener('comment-thread-mouseenter', e =>
- this._handleCommentThreadMouseenter(e)
+ diffTable.addEventListener(
+ 'comment-thread-mouseenter',
+ this.handleCommentThreadMouseenter
);
- this.addEventListener('create-comment-requested', e =>
- this._handleRangeCommentRequest(e)
+ diffTable.addEventListener(
+ 'create-comment-requested',
+ this.handleRangeCommentRequest
);
}
- override disconnectedCallback() {
+ cleanup() {
this.selectionChangeTask?.cancel();
- super.disconnectedCallback();
- }
-
- get diffBuilder() {
- if (!this._cachedDiffBuilder) {
- this._cachedDiffBuilder = this.querySelector(
- 'gr-diff-builder'
- ) as GrDiffBuilderElement;
+ if (this.diffTable) {
+ this.diffTable.removeEventListener(
+ 'comment-thread-mouseleave',
+ this.handleCommentThreadMouseleave
+ );
+ this.diffTable.removeEventListener(
+ 'comment-thread-mouseenter',
+ this.handleCommentThreadMouseenter
+ );
+ this.diffTable.removeEventListener(
+ 'create-comment-requested',
+ this.handleRangeCommentRequest
+ );
}
- return this._cachedDiffBuilder;
}
/**
@@ -129,18 +125,17 @@
// removed.
// If you wait longer than 50 ms, then you don't properly catch a very
// quick 'c' press after the selection change. If you wait less than 10
- // ms, then you will have about 50 _handleSelection calls when doing a
+ // ms, then you will have about 50 handleSelection() calls when doing a
// simple drag for select.
this.selectionChangeTask = debounce(
this.selectionChangeTask,
- () => this._handleSelection(selection, isMouseUp),
+ () => this.handleSelection(selection, isMouseUp),
10
);
}
- _getThreadEl(e: Event): GrDiffThreadElement | null {
- const path = (dom(e) as EventApi).path || [];
- for (const pathEl of path) {
+ private getThreadEl(e: Event): GrDiffThreadElement | null {
+ for (const pathEl of e.composedPath()) {
if (
pathEl instanceof HTMLElement &&
pathEl.classList.contains('comment-thread')
@@ -151,130 +146,74 @@
return null;
}
- _toggleRangeElHighlight(
- threadEl: GrDiffThreadElement,
+ private toggleRangeElHighlight(
+ threadEl: GrDiffThreadElement | null,
highlightRange = false
) {
- // We don't want to re-create the line just for highlighting the range which
- // is creating annoying bugs: @see Issue 12934
- // As gr-ranged-comment-layer now does not notify the layer re-render and
- // lack of access to the thread or the lineEl from the ranged-comment-layer,
- // need to update range class for styles here.
- let curNode: HTMLElement | null = threadEl.assignedSlot;
- while (curNode) {
- if (curNode.nodeName === 'TABLE') break;
- curNode = curNode.parentElement;
- }
- if (curNode?.querySelectorAll) {
- if (highlightRange) {
- const rangeNodes = curNode.querySelectorAll(
- `.range.${strToClassName(threadEl.rootId)}`
- );
- rangeNodes.forEach(rangeNode => {
- rangeNode.classList.add('rangeHoverHighlight');
- });
- const hintNode = threadEl.parentElement?.querySelector(
- `gr-ranged-comment-hint[threadElRootId="${threadEl.rootId}"]`
- );
- if (hintNode) {
- hintNode.shadowRoot
- ?.querySelectorAll('.rangeHighlight')
- .forEach(highlightNode =>
- highlightNode.classList.add('rangeHoverHighlight')
- );
- }
- } else {
- const rangeNodes = curNode.querySelectorAll(
- `.rangeHoverHighlight.${strToClassName(threadEl.rootId)}`
- );
- rangeNodes.forEach(rangeNode => {
- rangeNode.classList.remove('rangeHoverHighlight');
- });
- const hintNode = threadEl.parentElement?.querySelector(
- `gr-ranged-comment-hint[threadElRootId="${threadEl.rootId}"]`
- );
- if (hintNode) {
- hintNode.shadowRoot
- ?.querySelectorAll('.rangeHoverHighlight')
- .forEach(highlightNode =>
- highlightNode.classList.remove('rangeHoverHighlight')
- );
- }
- }
- }
- }
-
- _handleCommentThreadMouseenter(e: Event) {
- const threadEl = this._getThreadEl(e)!;
- const index = this._indexForThreadEl(threadEl);
-
- if (index !== undefined) {
- this.set(['commentRanges', index, 'hovering'], true);
- }
-
- this._toggleRangeElHighlight(threadEl, /* highlightRange= */ true);
- }
-
- _handleCommentThreadMouseleave(e: Event) {
- const threadEl = this._getThreadEl(e)!;
- const index = this._indexForThreadEl(threadEl);
-
- if (index !== undefined) {
- this.set(['commentRanges', index, 'hovering'], false);
- }
-
- this._toggleRangeElHighlight(threadEl, /* highlightRange= */ false);
- }
-
- _indexForThreadEl(threadEl: HTMLElement) {
- const side = getSide(threadEl);
- const range = getRange(threadEl);
- if (!side || !range) return undefined;
- return this._indexOfCommentRange(side, range);
- }
-
- _indexOfCommentRange(side: Side, range: CommentRange) {
- function rangesEqual(a: CommentRange, b: CommentRange) {
- if (!a && !b) {
- return true;
- }
- if (!a || !b) {
- return false;
- }
- return (
- a.start_line === b.start_line &&
- a.start_character === b.start_character &&
- a.end_line === b.end_line &&
- a.end_character === b.end_character
+ const rootId = threadEl?.rootId;
+ if (!rootId) return;
+ if (!this.diffTable) return;
+ if (highlightRange) {
+ const selector = `.range.${strToClassName(rootId)}`;
+ const rangeNodes = this.diffTable.querySelectorAll(selector);
+ rangeNodes.forEach(rangeNode => {
+ rangeNode.classList.add('rangeHoverHighlight');
+ });
+ const hintNode = this.diffTable.querySelector(
+ `gr-ranged-comment-hint[threadElRootId="${rootId}"]`
);
+ hintNode?.shadowRoot
+ ?.querySelectorAll('.rangeHighlight')
+ .forEach(highlightNode =>
+ highlightNode.classList.add('rangeHoverHighlight')
+ );
+ } else {
+ const selector = `.rangeHoverHighlight.${strToClassName(rootId)}`;
+ const rangeNodes = this.diffTable.querySelectorAll(selector);
+ rangeNodes.forEach(rangeNode => {
+ rangeNode.classList.remove('rangeHoverHighlight');
+ });
+ const hintNode = this.diffTable.querySelector(
+ `gr-ranged-comment-hint[threadElRootId="${rootId}"]`
+ );
+ hintNode?.shadowRoot
+ ?.querySelectorAll('.rangeHoverHighlight')
+ .forEach(highlightNode =>
+ highlightNode.classList.remove('rangeHoverHighlight')
+ );
}
-
- return this.commentRanges.findIndex(
- commentRange =>
- commentRange.side === side && rangesEqual(commentRange.range, range)
- );
}
+ private handleCommentThreadMouseenter = (e: Event) => {
+ const threadEl = this.getThreadEl(e);
+ this.toggleRangeElHighlight(threadEl, /* highlightRange= */ true);
+ };
+
+ private handleCommentThreadMouseleave = (e: Event) => {
+ const threadEl = this.getThreadEl(e);
+ this.toggleRangeElHighlight(threadEl, /* highlightRange= */ false);
+ };
+
/**
* Get current normalized selection.
* Merges multiple ranges, accounts for triple click, accounts for
* syntax highligh, convert native DOM Range objects to Gerrit concepts
* (line, side, etc).
*/
- _getNormalizedRange(selection: Selection | Range) {
+ private getNormalizedRange(selection: Selection | Range) {
/* On Safari the ShadowRoot.getSelection() isn't there and the only thing
we can get is a single Range */
if (selection instanceof Range) {
- return this._normalizeRange(selection);
+ return this.normalizeRange(selection);
}
const rangeCount = selection.rangeCount;
if (rangeCount === 0) {
return null;
} else if (rangeCount === 1) {
- return this._normalizeRange(selection.getRangeAt(0));
+ return this.normalizeRange(selection.getRangeAt(0));
} else {
- const startRange = this._normalizeRange(selection.getRangeAt(0));
- const endRange = this._normalizeRange(
+ const startRange = this.normalizeRange(selection.getRangeAt(0));
+ const endRange = this.normalizeRange(
selection.getRangeAt(rangeCount - 1)
);
return {
@@ -289,15 +228,15 @@
*
* @return fixed normalized range
*/
- _normalizeRange(domRange: Range): NormalizedRange {
+ private normalizeRange(domRange: Range): NormalizedRange {
const range = normalize(domRange);
- return this._fixTripleClickSelection(
+ return this.fixTripleClickSelection(
{
- start: this._normalizeSelectionSide(
+ start: this.normalizeSelectionSide(
range.startContainer,
range.startOffset
),
- end: this._normalizeSelectionSide(range.endContainer, range.endOffset),
+ end: this.normalizeSelectionSide(range.endContainer, range.endOffset),
},
domRange
);
@@ -313,7 +252,7 @@
* @param domRange DOM Range object
* @return fixed normalized range
*/
- _fixTripleClickSelection(range: NormalizedRange, domRange: Range) {
+ private fixTripleClickSelection(range: NormalizedRange, domRange: Range) {
if (!range.start) {
// Selection outside of current diff.
return range;
@@ -334,7 +273,7 @@
end.column === 0 &&
end.line === start.line + 1;
const content = domRange.cloneContents().querySelector('.contentText');
- const lineLength = (content && this._getLength(content)) || 0;
+ const lineLength = (content && this.getLength(content)) || 0;
if (lineLength && (endsAtBeginningOfNextLine || endsAtOtherEmptySide)) {
// Move the selection to the end of the previous line.
range.end = {
@@ -355,12 +294,14 @@
* @param node td.content child
* @param offset offset within node
*/
- _normalizeSelectionSide(
+ private normalizeSelectionSide(
node: Node | null,
offset: number
): NormalizedPosition | null {
let column;
- if (!node || !this.contains(node)) return null;
+ if (!this.diffTable) return null;
+ if (!this.diffBuilder) return null;
+ if (!node || !this.diffTable.contains(node)) return null;
const lineEl = getLineElByChild(node);
if (!lineEl) return null;
const side = getSideByLineEl(lineEl);
@@ -376,10 +317,10 @@
} else {
const thread = contentTd.querySelector('.comment-thread');
if (thread?.contains(node)) {
- column = this._getLength(contentText);
+ column = this.getLength(contentText);
node = contentText;
} else {
- column = this._convertOffsetToColumn(node, offset);
+ column = this.convertOffsetToColumn(node, offset);
}
}
@@ -398,7 +339,8 @@
* collapsed section, so don't need to worry about this case for
* positioning the tooltip.
*/
- _positionActionBox(
+ // visible for testing
+ positionActionBox(
actionBox: GrSelectionActionBox,
startLine: number,
range: Text | Element | Range
@@ -412,7 +354,7 @@
actionBox.placeBelow(range);
}
- _isRangeValid(range: NormalizedRange | null) {
+ private isRangeValid(range: NormalizedRange | null) {
if (!range || !range.start || !range.start.node || !range.end) {
return false;
}
@@ -425,15 +367,16 @@
);
}
- _handleSelection(selection: Selection | Range, isMouseUp: boolean) {
+ // visible for testing
+ handleSelection(selection: Selection | Range, isMouseUp: boolean) {
/* On Safari, the selection events may return a null range that should
be ignored */
- if (!selection) {
- return;
- }
- const normalizedRange = this._getNormalizedRange(selection);
- if (!this._isRangeValid(normalizedRange)) {
- this._removeActionBox();
+ if (!selection) return;
+ if (!this.diffTable) return;
+
+ const normalizedRange = this.getNormalizedRange(selection);
+ if (!this.isRangeValid(normalizedRange)) {
+ this.removeActionBox();
return;
}
/* On Safari the ShadowRoot.getSelection() isn't there and the only thing
@@ -463,8 +406,8 @@
// start.column with the content length), we just check if the selection
// is empty to see that it's at the end of a line.
const content = domRange.cloneContents().querySelector('.contentText');
- if (isMouseUp && this._getLength(content) === 0) {
- this._fireCreateRangeComment(start.side, {
+ if (isMouseUp && this.getLength(content) === 0) {
+ this.fireCreateRangeComment(start.side, {
start_line: start.line,
start_character: 0,
end_line: start.line,
@@ -474,10 +417,10 @@
return;
}
- let actionBox = this.shadowRoot!.querySelector('gr-selection-action-box');
+ let actionBox = this.diffTable.querySelector('gr-selection-action-box');
if (!actionBox) {
actionBox = document.createElement('gr-selection-action-box');
- this.root!.insertBefore(actionBox, this.root!.firstElementChild);
+ this.diffTable.appendChild(actionBox);
}
this.selectedRange = {
range: {
@@ -489,10 +432,10 @@
side: start.side,
};
if (start.line === end.line) {
- this._positionActionBox(actionBox, start.line, domRange);
+ this.positionActionBox(actionBox, start.line, domRange);
} else if (start.node instanceof Text) {
if (start.column) {
- this._positionActionBox(
+ this.positionActionBox(
actionBox,
start.line,
start.node.splitText(start.column)
@@ -505,44 +448,41 @@
(start.node.firstChild instanceof Element ||
start.node.firstChild instanceof Text)
) {
- this._positionActionBox(actionBox, start.line, start.node.firstChild);
+ this.positionActionBox(actionBox, start.line, start.node.firstChild);
} else if (start.node instanceof Element || start.node instanceof Text) {
- this._positionActionBox(actionBox, start.line, start.node);
+ this.positionActionBox(actionBox, start.line, start.node);
} else {
console.warn('Failed to position comment action box.');
- this._removeActionBox();
+ this.removeActionBox();
}
}
- _fireCreateRangeComment(side: Side, range: CommentRange) {
- this.dispatchEvent(
+ private fireCreateRangeComment(side: Side, range: CommentRange) {
+ this.diffTable?.dispatchEvent(
new CustomEvent('create-range-comment', {
detail: {side, range},
composed: true,
bubbles: true,
})
);
- this._removeActionBox();
+ this.removeActionBox();
}
- _handleRangeCommentRequest(e: Event) {
+ private handleRangeCommentRequest = (e: Event) => {
e.stopPropagation();
- if (!this.selectedRange) {
- throw Error('Selected Range is needed for new range comment!');
- }
+ assertIsDefined(this.selectedRange, 'selectedRange');
const {side, range} = this.selectedRange;
- this._fireCreateRangeComment(side, range);
- }
+ this.fireCreateRangeComment(side, range);
+ };
- _removeActionBox() {
+ // visible for testing
+ removeActionBox() {
this.selectedRange = undefined;
- const actionBox = this.shadowRoot!.querySelector('gr-selection-action-box');
- if (actionBox) {
- this.root!.removeChild(actionBox);
- }
+ const actionBox = this.diffTable?.querySelector('gr-selection-action-box');
+ if (actionBox) actionBox.remove();
}
- _convertOffsetToColumn(el: Node, offset: number) {
+ private convertOffsetToColumn(el: Node, offset: number) {
if (el instanceof Element && el.classList.contains('content')) {
return offset;
}
@@ -552,7 +492,7 @@
) {
if (el.previousSibling) {
el = el.previousSibling;
- offset += this._getLength(el);
+ offset += this.getLength(el);
} else {
el = el.parentElement!;
}
@@ -566,18 +506,24 @@
*
* @param node this is sometimes passed as null.
*/
- _getLength(node: Node | null): number {
+ // visible for testing
+ getLength(node: Node | null): number {
if (node === null) return 0;
if (node instanceof Element && node.classList.contains('content')) {
- return this._getLength(queryAndAssert(node, '.contentText'));
+ return this.getLength(queryAndAssert(node, '.contentText'));
} else {
return GrAnnotation.getLength(node);
}
}
}
+export interface CreateRangeCommentEventDetail {
+ side: Side;
+ range: CommentRange;
+}
+
declare global {
- interface HTMLElementTagNameMap {
- 'gr-diff-highlight': GrDiffHighlight;
+ interface HTMLElementEventMap {
+ 'create-range-comment': CustomEvent<CreateRangeCommentEventDetail>;
}
}
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight_html.ts b/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight_html.ts
deleted file mode 100644
index 5a6cb1c..0000000
--- a/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight_html.ts
+++ /dev/null
@@ -1,35 +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`
- <style include="shared-styles">
- :host {
- position: relative;
- }
- gr-selection-action-box {
- /**
- * Needs z-index to appear above wrapped content, since it's inserted
- * into DOM before it.
- */
- z-index: 10;
- }
- </style>
- <div class="contentWrapper">
- <slot></slot>
- </div>
-`;
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight_test.js b/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight_test.js
deleted file mode 100644
index 4c1295f..0000000
--- a/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight_test.js
+++ /dev/null
@@ -1,589 +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-highlight.js';
-import {_getTextOffset} from './gr-range-normalizer.js';
-import {html} from '@polymer/polymer/lib/utils/html-tag.js';
-
-// Splitting long lines in html into shorter rows breaks tests:
-// zero-length text nodes and new lines are not expected in some places
-/* eslint-disable max-len */
-const basicFixture = fixtureFromTemplate(html`
-<style>
- .tab-indicator:before {
- color: #C62828;
- /* >> character */
- content: '\\00BB';
- }
- </style>
- <gr-diff-highlight>
- <table id="diffTable">
-
- <tbody class="section both">
- <tr class="diff-row side-by-side" left-type="both" right-type="both">
- <td class="left lineNum" data-value="1"></td>
- <td class="content both"><div class="contentText">[1] Nam cum ad me in Cumanum salutandi causa uterque venisset,</div></td>
- <td class="right lineNum" data-value="1"></td>
- <td class="content both"><div class="contentText">[1] Nam cum ad me in Cumanum salutandi causa uterque</div></td>
- </tr>
- </tbody>
-
- <tbody class="section delta">
- <tr class="diff-row side-by-side" left-type="remove" right-type="add">
- <td class="left lineNum" data-value="2"></td>
- <!-- Next tag is formatted to eliminate zero-length text nodes. -->
- <td class="content remove"><div class="contentText">na💢ti <hl class="foo">te, inquit</hl>, sumus <hl class="bar">aliquando</hl> otiosum, <hl>certe</hl> a <hl><span class="tab-indicator" style="tab-size:8;">\u0009</span></hl>udiam, <hl>quid</hl> sit, <span class="tab-indicator" style="tab-size:8;">\u0009</span>quod <hl>Epicurum</hl></div></td>
- <td class="right lineNum" data-value="2"></td>
- <!-- Next tag is formatted to eliminate zero-length text nodes. -->
- <td class="content add"><div class="contentText">nacti , <hl>,</hl> sumus <hl><span class="tab-indicator" style="tab-size:8;">\u0009</span></hl> otiosum, <span class="tab-indicator" style="tab-size:8;">\u0009</span> audiam, sit, quod</div></td>
- </tr>
- </tbody>
-
-<tbody class="section both">
- <tr class="diff-row side-by-side" left-type="both" right-type="both">
- <td class="left lineNum" data-value="138"></td>
- <td class="content both"><div class="contentText">[14] Nam cum ad me in Cumanum salutandi causa uterque venisset,</div></td>
- <td class="right lineNum" data-value="119"></td>
- <td class="content both"><div class="contentText">[14] Nam cum ad me in Cumanum salutandi causa uterque venisset,</div></td>
- </tr>
- </tbody>
-
- <tbody class="section delta">
- <tr class="diff-row side-by-side" left-type="remove" right-type="add">
- <td class="left lineNum" data-value="140"></td>
- <!-- Next tag is formatted to eliminate zero-length text nodes. -->
- <td class="content remove"><div class="contentText">na💢ti <hl class="foo">te, inquit</hl>, sumus <hl class="bar">aliquando</hl> otiosum, <hl>certe</hl> a <hl><span class="tab-indicator" style="tab-size:8;">\u0009</span></hl>udiam, <hl>quid</hl> sit, <span class="tab-indicator" style="tab-size:8;">\u0009</span>quod <hl>Epicurum</hl></div><div class="comment-thread">
- [Yet another random diff thread content here]
- </div></td>
- <td class="right lineNum" data-value="120"></td>
- <!-- Next tag is formatted to eliminate zero-length text nodes. -->
- <td class="content add"><div class="contentText">nacti , <hl>,</hl> sumus <hl><span class="tab-indicator" style="tab-size:8;">\u0009</span></hl> otiosum, <span class="tab-indicator" style="tab-size:8;">\u0009</span> audiam, sit, quod</div></td>
- </tr>
- </tbody>
-
- <tbody class="section both">
- <tr class="diff-row side-by-side" left-type="both" right-type="both">
- <td class="left lineNum" data-value="141"></td>
- <td class="content both"><div class="contentText">nam et<hl><span class="tab-indicator" style="tab-size:8;">\u0009</span></hl>complectitur<span class="tab-indicator" style="tab-size:8;">\u0009</span>verbis, quod vult, et dicit plane, quod intellegam;</div></td>
- <td class="right lineNum" data-value="130"></td>
- <td class="content both"><div class="contentText">nam et complectitur verbis, quod vult, et dicit plane, quod intellegam;</div></td>
- </tr>
- </tbody>
-
- <tbody class="section contextControl">
- <tr class="diff-row side-by-side" left-type="contextControl" right-type="contextControl">
- <td class="left contextLineNum"></td>
- <td>
- <gr-button>+10↑</gr-button>
- -
- <gr-button>Show 21 common lines</gr-button>
- -
- <gr-button>+10↓</gr-button>
- </td>
- <td class="right contextLineNum"></td>
- <td>
- <gr-button>+10↑</gr-button>
- -
- <gr-button>Show 21 common lines</gr-button>
- -
- <gr-button>+10↓</gr-button>
- </td>
- </tr>
- </tbody>
-
- <tbody class="section delta total">
- <tr class="diff-row side-by-side" left-type="blank" right-type="add">
- <td class="left"></td>
- <td class="blank"></td>
- <td class="right lineNum" data-value="146"></td>
- <td class="content add"><div class="contentText">[17] Quid igitur est? inquit; audire enim cupio, quid non probes. Principio, inquam,</div></td>
- </tr>
- </tbody>
-
- <tbody class="section both">
- <tr class="diff-row side-by-side" left-type="both" right-type="both">
- <td class="left lineNum" data-value="165"></td>
- <td class="content both"><div class="contentText"></div></td>
- <td class="right lineNum" data-value="147"></td>
- <td class="content both"><div class="contentText">in physicis, <hl><span class="tab-indicator" style="tab-size:8;">\u0009</span></hl> quibus maxime gloriatur, primum totus est alienus. Democritea dicit</div></td>
- </tr>
- </tbody>
-
- </table>
- </gr-diff-highlight>
-`);
-/* eslint-enable max-len */
-
-suite('gr-diff-highlight', () => {
- let element;
-
- setup(() => {
- element = basicFixture.instantiate()[1];
- });
-
- suite('comment events', () => {
- let builder;
-
- setup(() => {
- builder = {
- getContentsByLineRange: sinon.stub().returns([]),
- getLineElByChild: sinon.stub().returns({}),
- getSideByLineEl: sinon.stub().returns('other-side'),
- };
- element._cachedDiffBuilder = builder;
- });
-
- test('comment-thread-mouseenter from line comments is ignored', () => {
- const threadEl = document.createElement('div');
- threadEl.className = 'comment-thread';
- threadEl.setAttribute('diff-side', 'right');
- threadEl.setAttribute('line-num', 3);
- element.appendChild(threadEl);
- element.commentRanges = [{side: 'right'}];
-
- sinon.stub(element, 'set');
- threadEl.dispatchEvent(new CustomEvent(
- 'comment-thread-mouseenter', {bubbles: true, composed: true}));
- assert.isFalse(element.set.called);
- });
-
- test('comment-thread-mouseenter from ranged comment causes set', () => {
- const threadEl = document.createElement('div');
- threadEl.className = 'comment-thread';
- threadEl.setAttribute('diff-side', 'right');
- threadEl.setAttribute('line-num', 3);
- threadEl.setAttribute('range', JSON.stringify({
- start_line: 3,
- start_character: 4,
- end_line: 5,
- end_character: 6,
- }));
- element.appendChild(threadEl);
- element.commentRanges = [{side: 'right', range: {
- start_line: 3,
- start_character: 4,
- end_line: 5,
- end_character: 6,
- }}];
-
- sinon.stub(element, 'set');
- threadEl.dispatchEvent(new CustomEvent(
- 'comment-thread-mouseenter', {bubbles: true, composed: true}));
- assert.isTrue(element.set.called);
- const args = element.set.lastCall.args;
- assert.deepEqual(args[0], ['commentRanges', 0, 'hovering']);
- assert.deepEqual(args[1], true);
- });
-
- test('comment-thread-mouseleave from line comments is ignored', () => {
- const threadEl = document.createElement('div');
- threadEl.className = 'comment-thread';
- threadEl.setAttribute('diff-side', 'right');
- threadEl.setAttribute('line-num', 3);
- element.appendChild(threadEl);
- element.commentRanges = [{side: 'right'}];
-
- sinon.stub(element, 'set');
- threadEl.dispatchEvent(new CustomEvent(
- 'comment-thread-mouseleave', {bubbles: true, composed: true}));
- assert.isFalse(element.set.called);
- });
-
- test(`create-range-comment for range when create-comment-requested
- is fired`, () => {
- sinon.stub(element, '_removeActionBox');
- element.selectedRange = {
- side: 'left',
- range: {
- start_line: 7,
- start_character: 11,
- end_line: 24,
- end_character: 42,
- },
- };
- const requestEvent = new CustomEvent('create-comment-requested');
- let createRangeEvent;
- element.addEventListener('create-range-comment', e => {
- createRangeEvent = e;
- });
- element.dispatchEvent(requestEvent);
- assert.deepEqual(element.selectedRange, createRangeEvent.detail);
- assert.isTrue(element._removeActionBox.called);
- });
- });
-
- suite('selection', () => {
- let diff;
- let builder;
- let contentStubs;
-
- const stubContent = (line, side, opt_child) => {
- const contentTd = diff.querySelector(
- `.${side}.lineNum[data-value="${line}"] ~ .content`);
- const contentText = contentTd.querySelector('.contentText');
- const lineEl = diff.querySelector(
- `.${side}.lineNum[data-value="${line}"]`);
- contentStubs.push({
- lineEl,
- contentTd,
- contentText,
- });
- builder.getContentTdByLineEl.withArgs(lineEl).returns(contentTd);
- builder.getLineNumberByChild.withArgs(lineEl).returns(line);
- builder.getContentTdByLine.withArgs(line, side).returns(contentTd);
- builder.getSideByLineEl.withArgs(lineEl).returns(side);
- return contentText;
- };
-
- const emulateSelection = (startNode, startOffset, endNode, endOffset) => {
- const selection = document.getSelection();
- const range = document.createRange();
- range.setStart(startNode, startOffset);
- range.setEnd(endNode, endOffset);
- selection.addRange(range);
- element._handleSelection(selection);
- };
-
- const getLineElByChild = node => {
- const stubs = contentStubs.find(stub => stub.contentTd.contains(node));
- return stubs && stubs.lineEl;
- };
-
- setup(() => {
- contentStubs = [];
- stub('gr-selection-action-box', 'placeAbove');
- stub('gr-selection-action-box', 'placeBelow');
- diff = element.querySelector('#diffTable');
- builder = {
- getContentTdByLine: sinon.stub(),
- getContentTdByLineEl: sinon.stub(),
- getLineElByChild,
- getLineNumberByChild: sinon.stub(),
- getSideByLineEl: sinon.stub(),
- };
- element._cachedDiffBuilder = builder;
- });
-
- teardown(() => {
- contentStubs = null;
- document.getSelection().removeAllRanges();
- });
-
- test('single first line', () => {
- const content = stubContent(1, 'right');
- sinon.spy(element, '_positionActionBox');
- emulateSelection(content.firstChild, 5, content.firstChild, 12);
- const actionBox = element.shadowRoot
- .querySelector('gr-selection-action-box');
- assert.isTrue(actionBox.positionBelow);
- });
-
- test('multiline starting on first line', () => {
- const startContent = stubContent(1, 'right');
- const endContent = stubContent(2, 'right');
- sinon.spy(element, '_positionActionBox');
- emulateSelection(
- startContent.firstChild, 10, endContent.lastChild, 7);
- const actionBox = element.shadowRoot
- .querySelector('gr-selection-action-box');
- assert.isTrue(actionBox.positionBelow);
- });
-
- test('single line', () => {
- const content = stubContent(138, 'left');
- sinon.spy(element, '_positionActionBox');
- emulateSelection(content.firstChild, 5, content.firstChild, 12);
- const actionBox = element.shadowRoot
- .querySelector('gr-selection-action-box');
- const {range, side} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 138,
- start_character: 5,
- end_line: 138,
- end_character: 12,
- });
- assert.equal(side, 'left');
- assert.notOk(actionBox.positionBelow);
- });
-
- test('multiline', () => {
- const startContent = stubContent(119, 'right');
- const endContent = stubContent(120, 'right');
- sinon.spy(element, '_positionActionBox');
- emulateSelection(
- startContent.firstChild, 10, endContent.lastChild, 7);
- const actionBox = element.shadowRoot
- .querySelector('gr-selection-action-box');
- const {range, side} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 119,
- start_character: 10,
- end_line: 120,
- end_character: 36,
- });
- assert.equal(side, 'right');
- assert.notOk(actionBox.positionBelow);
- });
-
- test('multiple ranges aka firefox implementation', () => {
- const startContent = stubContent(119, 'right');
- const endContent = stubContent(120, 'right');
-
- const startRange = document.createRange();
- startRange.setStart(startContent.firstChild, 10);
- startRange.setEnd(startContent.firstChild, 11);
-
- const endRange = document.createRange();
- endRange.setStart(endContent.lastChild, 6);
- endRange.setEnd(endContent.lastChild, 7);
-
- const getRangeAtStub = sinon.stub();
- getRangeAtStub
- .onFirstCall().returns(startRange)
- .onSecondCall()
- .returns(endRange);
- const selection = {
- rangeCount: 2,
- getRangeAt: getRangeAtStub,
- removeAllRanges: sinon.stub(),
- };
- element._handleSelection(selection);
- const {range} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 119,
- start_character: 10,
- end_line: 120,
- end_character: 36,
- });
- });
-
- test('multiline grow end highlight over tabs', () => {
- const startContent = stubContent(119, 'right');
- const endContent = stubContent(120, 'right');
- emulateSelection(startContent.firstChild, 10, endContent.firstChild, 2);
- const {range, side} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 119,
- start_character: 10,
- end_line: 120,
- end_character: 2,
- });
- assert.equal(side, 'right');
- });
-
- test('collapsed', () => {
- const content = stubContent(138, 'left');
- emulateSelection(content.firstChild, 5, content.firstChild, 5);
- assert.isOk(document.getSelection().getRangeAt(0).startContainer);
- assert.isFalse(!!element.selectedRange);
- });
-
- test('starts inside hl', () => {
- const content = stubContent(140, 'left');
- const hl = content.querySelector('.foo');
- emulateSelection(hl.firstChild, 2, hl.nextSibling, 7);
- const {range, side} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 140,
- start_character: 8,
- end_line: 140,
- end_character: 23,
- });
- assert.equal(side, 'left');
- });
-
- test('ends inside hl', () => {
- const content = stubContent(140, 'left');
- const hl = content.querySelector('.bar');
- emulateSelection(hl.previousSibling, 2, hl.firstChild, 3);
- const {range} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 140,
- start_character: 18,
- end_line: 140,
- end_character: 27,
- });
- });
-
- test('multiple hl', () => {
- const content = stubContent(140, 'left');
- const hl = content.querySelectorAll('hl')[4];
- emulateSelection(content.firstChild, 2, hl.firstChild, 2);
- const {range, side} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 140,
- start_character: 2,
- end_line: 140,
- end_character: 61,
- });
- assert.equal(side, 'left');
- });
-
- test('starts outside of diff', () => {
- const contentText = stubContent(140, 'left');
- const contentTd = contentText.parentElement;
-
- emulateSelection(contentTd.parentElement, 0,
- contentText.firstChild, 2);
- assert.isFalse(!!element.selectedRange);
- });
-
- test('ends outside of diff', () => {
- const content = stubContent(140, 'left');
- emulateSelection(content.nextElementSibling.firstChild, 2,
- content.firstChild, 2);
- assert.isFalse(!!element.selectedRange);
- });
-
- test('starts and ends on different sides', () => {
- const startContent = stubContent(140, 'left');
- const endContent = stubContent(130, 'right');
- emulateSelection(startContent.firstChild, 2, endContent.firstChild, 2);
- assert.isFalse(!!element.selectedRange);
- });
-
- test('starts in comment thread element', () => {
- const startContent = stubContent(140, 'left');
- const comment = startContent.parentElement.querySelector(
- '.comment-thread');
- const endContent = stubContent(141, 'left');
- emulateSelection(comment.firstChild, 2, endContent.firstChild, 4);
- const {range, side} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 140,
- start_character: 83,
- end_line: 141,
- end_character: 4,
- });
- assert.equal(side, 'left');
- });
-
- test('ends in comment thread element', () => {
- const content = stubContent(140, 'left');
- const comment = content.parentElement.querySelector(
- '.comment-thread');
- emulateSelection(content.firstChild, 4, comment.firstChild, 1);
- const {range, side} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 140,
- start_character: 4,
- end_line: 140,
- end_character: 83,
- });
- assert.equal(side, 'left');
- });
-
- test('starts in context element', () => {
- const contextControl =
- diff.querySelector('.contextControl').querySelector('gr-button');
- const content = stubContent(146, 'right');
- emulateSelection(contextControl, 0, content.firstChild, 7);
- // TODO (viktard): Select nearest line.
- assert.isFalse(!!element.selectedRange);
- });
-
- test('ends in context element', () => {
- const contextControl =
- diff.querySelector('.contextControl').querySelector('gr-button');
- const content = stubContent(141, 'left');
- emulateSelection(content.firstChild, 2, contextControl, 1);
- // TODO (viktard): Select nearest line.
- assert.isFalse(!!element.selectedRange);
- });
-
- test('selection containing context element', () => {
- const startContent = stubContent(130, 'right');
- const endContent = stubContent(146, 'right');
- emulateSelection(startContent.firstChild, 3, endContent.firstChild, 14);
- const {range, side} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 130,
- start_character: 3,
- end_line: 146,
- end_character: 14,
- });
- assert.equal(side, 'right');
- });
-
- test('ends at a tab', () => {
- const content = stubContent(140, 'left');
- emulateSelection(
- content.firstChild, 1, content.querySelector('span'), 0);
- const {range, side} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 140,
- start_character: 1,
- end_line: 140,
- end_character: 51,
- });
- assert.equal(side, 'left');
- });
-
- test('starts at a tab', () => {
- const content = stubContent(140, 'left');
- emulateSelection(
- content.querySelectorAll('hl')[3], 0,
- content.querySelectorAll('span')[1].nextSibling, 1);
- const {range, side} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 140,
- start_character: 51,
- end_line: 140,
- end_character: 71,
- });
- assert.equal(side, 'left');
- });
-
- test('properly accounts for syntax highlighting', () => {
- const content = stubContent(140, 'left');
- const spy = sinon.spy(element, '_normalizeRange');
- emulateSelection(
- content.querySelectorAll('hl')[3], 0,
- content.querySelectorAll('span')[1], 0);
- const spyCall = spy.getCall(0);
- const range = document.getSelection().getRangeAt(0);
- assert.notDeepEqual(spyCall.returnValue, range);
- });
-
- test('GrRangeNormalizer._getTextOffset computes text offset', () => {
- let content = stubContent(140, 'left');
- let child = content.lastChild.lastChild;
- let result = _getTextOffset(content, child);
- assert.equal(result, 75);
- content = stubContent(146, 'right');
- child = content.lastChild;
- result = _getTextOffset(content, child);
- assert.equal(result, 0);
- });
-
- test('_fixTripleClickSelection', () => {
- const startContent = stubContent(119, 'right');
- const endContent = stubContent(120, 'right');
- emulateSelection(startContent.firstChild, 0, endContent.firstChild, 0);
- const {range, side} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 119,
- start_character: 0,
- end_line: 119,
- end_character: element._getLength(startContent),
- });
- assert.equal(side, 'right');
- });
- });
-});
-
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight_test.ts
new file mode 100644
index 0000000..b819754
--- /dev/null
+++ b/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight_test.ts
@@ -0,0 +1,713 @@
+/**
+ * @license
+ * Copyright 2016 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup-karma';
+import './gr-diff-highlight';
+import {_getTextOffset} from './gr-range-normalizer';
+import {fixture, fixtureCleanup, html} from '@open-wc/testing-helpers';
+import {
+ GrDiffHighlight,
+ DiffBuilderInterface,
+ CreateRangeCommentEventDetail,
+} from './gr-diff-highlight';
+import {Side} from '../../../api/diff';
+import {SinonStubbedMember} from 'sinon';
+import {queryAndAssert} from '../../../utils/common-util';
+import {GrDiffThreadElement} from '../gr-diff/gr-diff-utils';
+import {waitQueryAndAssert, waitUntil} from '../../../test/test-utils';
+import {GrSelectionActionBox} from '../gr-selection-action-box/gr-selection-action-box';
+
+// Splitting long lines in html into shorter rows breaks tests:
+// zero-length text nodes and new lines are not expected in some places
+/* eslint-disable max-len, lit/prefer-static-styles */
+/* prettier-ignore */
+const diffTable = html`
+ <table id="diffTable">
+ <tbody class="section both">
+ <tr class="diff-row side-by-side" left-type="both" right-type="both">
+ <td class="left lineNum" data-value="1"></td>
+ <td class="content both"><div class="contentText">[1] Nam cum ad me in Cumanum salutandi causa uterque venisset,</div></td>
+ <td class="right lineNum" data-value="1"></td>
+ <td class="content both"><div class="contentText">[1] Nam cum ad me in Cumanum salutandi causa uterque</div></td>
+ </tr>
+ </tbody>
+
+ <tbody class="section delta">
+ <tr class="diff-row side-by-side" left-type="remove" right-type="add">
+ <td class="left lineNum" data-value="2"></td>
+ <!-- Next tag is formatted to eliminate zero-length text nodes. -->
+ <td class="content remove"><div class="contentText">na💢ti <hl class="foo range generated_id314">te, inquit</hl>, sumus<hl class="bar">aliquando</hl> otiosum, <hl>certe</hl> a<hl><span class="tab-indicator" style="tab-size:8;"> </span></hl>udiam, <hl>quid</hl> sit,<span class="tab-indicator" style="tab-size:8;"> </span>quod<hl>Epicurum</hl></div></td>
+ <td class="right lineNum" data-value="2"></td>
+ <!-- Next tag is formatted to eliminate zero-length text nodes. -->
+ <td class="content add"><div class="contentText">nacti , <hl>,</hl> sumus<hl><span class="tab-indicator" style="tab-size:8;"> </span></hl>otiosum,<span class="tab-indicator" style="tab-size:8;"> </span> audiam,sit, quod</div></td>
+ </tr>
+ </tbody>
+
+ <tbody class="section both">
+ <tr class="diff-row side-by-side" left-type="both" right-type="both">
+ <td class="left lineNum" data-value="138"></td>
+ <td class="content both"><div class="contentText">[14] Nam cum ad me in Cumanum salutandi causa uterque venisset,</div></td>
+ <td class="right lineNum" data-value="119"></td>
+ <td class="content both"><div class="contentText">[14] Nam cum ad me in Cumanum salutandi causa uterque venisset,</div></td>
+ </tr>
+ </tbody>
+
+ <tbody class="section delta">
+ <tr class="diff-row side-by-side" left-type="remove" right-type="add">
+ <td class="left lineNum" data-value="140"></td>
+ <!-- Next tag is formatted to eliminate zero-length text nodes. -->
+ <td class="content remove"><div class="contentText">na💢ti <hl class="foo">te, inquit</hl>, sumus <hl class="bar">aliquando</hl> otiosum, <hl>certe</hl> a <hl><span class="tab-indicator" style="tab-size:8;">\u0009</span></hl>udiam, <hl>quid</hl> sit, <span class="tab-indicator" style="tab-size:8;">\u0009</span>quod <hl>Epicurum</hl></div><div class="comment-thread">
+ [Yet another random diff thread content here]
+ </div></td>
+ <td class="right lineNum" data-value="120"></td>
+ <!-- Next tag is formatted to eliminate zero-length text nodes. -->
+ <td class="content add"><div class="contentText">nacti , <hl>,</hl> sumus <hl><span class="tab-indicator" style="tab-size:8;">\u0009</span></hl> otiosum, <span class="tab-indicator" style="tab-size:8;">\u0009</span> audiam, sit, quod</div></td>
+ </tr>
+ </tbody>
+
+ <tbody class="section both">
+ <tr class="diff-row side-by-side" left-type="both" right-type="both">
+ <td class="left lineNum" data-value="141"></td>
+ <td class="content both"><div class="contentText">nam et<hl><span class="tab-indicator" style="tab-size:8;"> </span></hl>complectitur<span class="tab-indicator" style="tab-size:8;"></span>verbis, quod vult, et dicit plane, quod intellegam;</div></td>
+ <td class="right lineNum" data-value="130"></td>
+ <td class="content both"><div class="contentText">nam et complectitur verbis, quod vult, et dicit plane, quodintellegam;</div></td>
+ </tr>
+ </tbody>
+
+ <tbody class="section contextControl">
+ <tr
+ class="diff-row side-by-side"
+ left-type="contextControl"
+ right-type="contextControl"
+ >
+ <td class="left contextLineNum"></td>
+ <td>
+ <gr-button>+10↑</gr-button>
+ -
+ <gr-button>Show 21 common lines</gr-button>
+ -
+ <gr-button>+10↓</gr-button>
+ </td>
+ <td class="right contextLineNum"></td>
+ <td>
+ <gr-button>+10↑</gr-button>
+ -
+ <gr-button>Show 21 common lines</gr-button>
+ -
+ <gr-button>+10↓</gr-button>
+ </td>
+ </tr>
+ </tbody>
+
+ <tbody class="section delta total">
+ <tr class="diff-row side-by-side" left-type="blank" right-type="add">
+ <td class="left"></td>
+ <td class="blank"></td>
+ <td class="right lineNum" data-value="146"></td>
+ <td class="content add"><div class="contentText">[17] Quid igitur est? inquit; audire enim cupio, quid non probes. Principio, inquam,</div></td>
+ </tr>
+ </tbody>
+
+ <tbody class="section both">
+ <tr class="diff-row side-by-side" left-type="both" right-type="both">
+ <td class="left lineNum" data-value="165"></td>
+ <td class="content both"><div class="contentText"></div></td>
+ <td class="right lineNum" data-value="147"></td>
+ <td class="content both"><div class="contentText">in physicis, <hl><span class="tab-indicator" style="tab-size:8;"> </span></hl>quibus maxime gloriatur, primum totus est alienus. Democritea dicit</div></td>
+ </tr>
+ </tbody>
+ </table>
+`;
+/* eslint-enable max-len */
+
+suite('gr-diff-highlight', () => {
+ suite('comment events', () => {
+ let threadEl: GrDiffThreadElement;
+ let hlRange: HTMLElement;
+ let element: GrDiffHighlight;
+ let diff: HTMLElement;
+ let builder: {
+ getContentTdByLineEl: SinonStubbedMember<
+ DiffBuilderInterface['getContentTdByLineEl']
+ >;
+ };
+
+ setup(async () => {
+ diff = await fixture<HTMLTableElement>(diffTable);
+ builder = {
+ getContentTdByLineEl: sinon.stub(),
+ };
+ element = new GrDiffHighlight();
+ element.init(diff, builder);
+ hlRange = queryAndAssert(diff, 'hl.range.generated_id314');
+
+ threadEl = document.createElement(
+ 'div'
+ ) as unknown as GrDiffThreadElement;
+ threadEl.className = 'comment-thread';
+ threadEl.rootId = 'id314';
+ diff.appendChild(threadEl);
+ });
+
+ teardown(() => {
+ element.cleanup();
+ threadEl.remove();
+ });
+
+ test('comment-thread-mouseenter toggles rangeHoverHighlight class', async () => {
+ assert.isFalse(hlRange.classList.contains('rangeHoverHighlight'));
+ threadEl.dispatchEvent(
+ new CustomEvent('comment-thread-mouseenter', {
+ bubbles: true,
+ composed: true,
+ })
+ );
+ await waitUntil(() => hlRange.classList.contains('rangeHoverHighlight'));
+ assert.isTrue(hlRange.classList.contains('rangeHoverHighlight'));
+ });
+
+ test('comment-thread-mouseleave toggles rangeHoverHighlight class', async () => {
+ hlRange.classList.add('rangeHoverHighlight');
+ threadEl.dispatchEvent(
+ new CustomEvent('comment-thread-mouseleave', {
+ bubbles: true,
+ composed: true,
+ })
+ );
+ await waitUntil(() => !hlRange.classList.contains('rangeHoverHighlight'));
+ assert.isFalse(hlRange.classList.contains('rangeHoverHighlight'));
+ });
+
+ test(`create-range-comment for range when create-comment-requested
+ is fired`, () => {
+ const removeActionBoxStub = sinon.stub(element, 'removeActionBox');
+ element.selectedRange = {
+ side: Side.LEFT,
+ range: {
+ start_line: 7,
+ start_character: 11,
+ end_line: 24,
+ end_character: 42,
+ },
+ };
+ const requestEvent = new CustomEvent('create-comment-requested');
+ let createRangeEvent: CustomEvent<CreateRangeCommentEventDetail>;
+ diff.addEventListener('create-range-comment', e => {
+ createRangeEvent = e;
+ });
+ diff.dispatchEvent(requestEvent);
+ if (!createRangeEvent!) assert.fail('event not set');
+ assert.deepEqual(element.selectedRange, createRangeEvent.detail);
+ assert.isTrue(removeActionBoxStub.called);
+ });
+ });
+
+ suite('selection', () => {
+ let element: GrDiffHighlight;
+ let diff: HTMLElement;
+ let builder: {
+ getContentTdByLineEl: SinonStubbedMember<
+ DiffBuilderInterface['getContentTdByLineEl']
+ >;
+ };
+ let contentStubs;
+
+ setup(async () => {
+ diff = await fixture<HTMLTableElement>(diffTable);
+ builder = {
+ getContentTdByLineEl: sinon.stub(),
+ };
+ element = new GrDiffHighlight();
+ element.init(diff, builder);
+ contentStubs = [];
+ stub('gr-selection-action-box', 'placeAbove');
+ stub('gr-selection-action-box', 'placeBelow');
+ });
+
+ teardown(() => {
+ fixtureCleanup();
+ element.cleanup();
+ contentStubs = null;
+ document.getSelection()!.removeAllRanges();
+ });
+
+ const stubContent = (line: number, side: Side) => {
+ const contentTd = diff.querySelector(
+ `.${side}.lineNum[data-value="${line}"] ~ .content`
+ );
+ if (!contentTd) assert.fail('content td not found');
+ const contentText = contentTd.querySelector('.contentText');
+ const lineEl =
+ diff.querySelector(`.${side}.lineNum[data-value="${line}"]`) ??
+ undefined;
+ contentStubs.push({
+ lineEl,
+ contentTd,
+ contentText,
+ });
+ builder.getContentTdByLineEl.withArgs(lineEl).returns(contentTd);
+ return contentText;
+ };
+
+ const emulateSelection = (
+ startNode: Node,
+ startOffset: number,
+ endNode: Node,
+ endOffset: number
+ ) => {
+ const selection = document.getSelection();
+ if (!selection) assert.fail('no selection');
+ selection.removeAllRanges();
+ const range = document.createRange();
+ range.setStart(startNode, startOffset);
+ range.setEnd(endNode, endOffset);
+ selection.addRange(range);
+ element.handleSelection(selection, false);
+ };
+
+ test('single first line', () => {
+ const content = stubContent(1, Side.RIGHT);
+ sinon.spy(element, 'positionActionBox');
+ if (!content?.firstChild) assert.fail('content first child not found');
+ emulateSelection(content.firstChild, 5, content.firstChild, 12);
+ const actionBox = diff.querySelector('gr-selection-action-box');
+ if (!actionBox) assert.fail('action box not found');
+ assert.isTrue(actionBox.positionBelow);
+ });
+
+ test('multiline starting on first line', () => {
+ const startContent = stubContent(1, Side.RIGHT);
+ const endContent = stubContent(2, Side.RIGHT);
+ sinon.spy(element, 'positionActionBox');
+ if (!startContent?.firstChild) {
+ assert.fail('first child of start content not found');
+ }
+ if (!endContent?.lastChild) {
+ assert.fail('last child of end content not found');
+ }
+ emulateSelection(startContent.firstChild, 10, endContent.lastChild, 7);
+ const actionBox = diff.querySelector('gr-selection-action-box');
+ if (!actionBox) assert.fail('action box not found');
+ assert.isTrue(actionBox.positionBelow);
+ });
+
+ test('single line', async () => {
+ const content = stubContent(138, Side.LEFT);
+ sinon.spy(element, 'positionActionBox');
+ if (!content?.firstChild) assert.fail('content first child not found');
+ emulateSelection(content.firstChild, 5, content.firstChild, 12);
+ const actionBox = await waitQueryAndAssert<GrSelectionActionBox>(
+ diff,
+ 'gr-selection-action-box'
+ );
+ if (!element.selectedRange) assert.fail('no range selected');
+ const {range, side} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 138,
+ start_character: 5,
+ end_line: 138,
+ end_character: 12,
+ });
+ assert.equal(side, Side.LEFT);
+ assert.notOk(actionBox.positionBelow);
+ });
+
+ test('multiline', () => {
+ const startContent = stubContent(119, Side.RIGHT);
+ const endContent = stubContent(120, Side.RIGHT);
+ sinon.spy(element, 'positionActionBox');
+ if (!startContent?.firstChild) {
+ assert.fail('first child of start content not found');
+ }
+ if (!endContent?.lastChild) {
+ assert.fail('last child of end content');
+ }
+ emulateSelection(startContent.firstChild, 10, endContent.lastChild, 7);
+ const actionBox = diff.querySelector('gr-selection-action-box');
+ if (!actionBox) assert.fail('action box not found');
+ if (!element.selectedRange) assert.fail('no range selected');
+ const {range, side} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 119,
+ start_character: 10,
+ end_line: 120,
+ end_character: 36,
+ });
+ assert.equal(side, Side.RIGHT);
+ assert.notOk(actionBox.positionBelow);
+ });
+
+ test('multiple ranges aka firefox implementation', () => {
+ const startContent = stubContent(119, Side.RIGHT);
+ const endContent = stubContent(120, Side.RIGHT);
+ if (!startContent?.firstChild) {
+ assert.fail('first child of start content not found');
+ }
+ if (!endContent?.lastChild) {
+ assert.fail('last child of end content');
+ }
+
+ const startRange = document.createRange();
+ startRange.setStart(startContent.firstChild, 10);
+ startRange.setEnd(startContent.firstChild, 11);
+
+ const endRange = document.createRange();
+ endRange.setStart(endContent.lastChild, 6);
+ endRange.setEnd(endContent.lastChild, 7);
+
+ const getRangeAtStub = sinon.stub();
+ getRangeAtStub
+ .onFirstCall()
+ .returns(startRange)
+ .onSecondCall()
+ .returns(endRange);
+ const selection = {
+ rangeCount: 2,
+ getRangeAt: getRangeAtStub,
+ removeAllRanges: sinon.stub(),
+ } as unknown as Selection;
+ element.handleSelection(selection, false);
+ if (!element.selectedRange) assert.fail('no range selected');
+ const {range} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 119,
+ start_character: 10,
+ end_line: 120,
+ end_character: 36,
+ });
+ });
+
+ test('multiline grow end highlight over tabs', () => {
+ const startContent = stubContent(119, Side.RIGHT);
+ const endContent = stubContent(120, Side.RIGHT);
+ if (!startContent?.firstChild) {
+ assert.fail('first child of start content not found');
+ }
+ if (!endContent?.firstChild) {
+ assert.fail('first child of end content not found');
+ }
+ emulateSelection(startContent.firstChild, 10, endContent.firstChild, 2);
+ if (!element.selectedRange) assert.fail('no range selected');
+ const {range, side} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 119,
+ start_character: 10,
+ end_line: 120,
+ end_character: 2,
+ });
+ assert.equal(side, Side.RIGHT);
+ });
+
+ test('collapsed', () => {
+ const content = stubContent(138, Side.LEFT);
+ if (!content?.firstChild) {
+ assert.fail('first child of content not found');
+ }
+ emulateSelection(content.firstChild, 5, content.firstChild, 5);
+ const sel = document.getSelection();
+ if (!sel) assert.fail('no selection');
+ assert.isOk(sel.getRangeAt(0).startContainer);
+ assert.isFalse(!!element.selectedRange);
+ });
+
+ test('starts inside hl', () => {
+ const content = stubContent(140, Side.LEFT);
+ if (!content) {
+ assert.fail('content not found');
+ }
+ const hl = content.querySelector('.foo');
+ if (!hl?.firstChild) {
+ assert.fail('first child of hl element not found');
+ }
+ if (!hl?.nextSibling) {
+ assert.fail('next sibling of hl element not found');
+ }
+ emulateSelection(hl.firstChild, 2, hl.nextSibling, 7);
+ if (!element.selectedRange) assert.fail('no range selected');
+ const {range, side} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 140,
+ start_character: 8,
+ end_line: 140,
+ end_character: 23,
+ });
+ assert.equal(side, Side.LEFT);
+ });
+
+ test('ends inside hl', () => {
+ const content = stubContent(140, Side.LEFT);
+ if (!content) assert.fail('content not found');
+ const hl = content.querySelector('.bar');
+ if (!hl) assert.fail('hl inside content not found');
+ if (!hl.previousSibling) assert.fail('previous sibling not found');
+ if (!hl.firstChild) assert.fail('first child not found');
+ emulateSelection(hl.previousSibling, 2, hl.firstChild, 3);
+ if (!element.selectedRange) assert.fail('no range selected');
+ const {range} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 140,
+ start_character: 18,
+ end_line: 140,
+ end_character: 27,
+ });
+ });
+
+ test('multiple hl', () => {
+ const content = stubContent(140, Side.LEFT);
+ if (!content) assert.fail('content not found');
+ if (!content.firstChild) assert.fail('first child not found');
+ const hl = content.querySelectorAll('hl')[4];
+ if (!hl) assert.fail('hl not found');
+ if (!hl.firstChild) assert.fail('first child of hl not found');
+ emulateSelection(content.firstChild, 2, hl.firstChild, 2);
+ if (!element.selectedRange) assert.fail('no range selected');
+ const {range, side} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 140,
+ start_character: 2,
+ end_line: 140,
+ end_character: 61,
+ });
+ assert.equal(side, Side.LEFT);
+ });
+
+ test('starts outside of diff', () => {
+ const contentText = stubContent(140, Side.LEFT);
+ if (!contentText) assert.fail('content not found');
+ if (!contentText.firstChild) assert.fail('child not found');
+ const contentTd = contentText.parentElement;
+ if (!contentTd) assert.fail('content td not found');
+ if (!contentTd.parentElement) assert.fail('parent of td not found');
+
+ emulateSelection(contentTd.parentElement, 0, contentText.firstChild, 2);
+ assert.isFalse(!!element.selectedRange);
+ });
+
+ test('ends outside of diff', () => {
+ const content = stubContent(140, Side.LEFT);
+ if (!content) assert.fail('content not found');
+ if (!content.firstChild) assert.fail('child not found');
+ if (!content.nextElementSibling) assert.fail('sibling not found');
+ if (!content.nextElementSibling.firstChild) {
+ assert.fail('sibling child not found');
+ }
+ emulateSelection(
+ content.nextElementSibling.firstChild,
+ 2,
+ content.firstChild,
+ 2
+ );
+ assert.isFalse(!!element.selectedRange);
+ });
+
+ test('starts and ends on different sides', () => {
+ const startContent = stubContent(140, Side.LEFT);
+ const endContent = stubContent(130, Side.RIGHT);
+ if (!startContent?.firstChild) {
+ assert.fail('first child of start content not found');
+ }
+ if (!endContent?.firstChild) {
+ assert.fail('first child of end content not found');
+ }
+ emulateSelection(startContent.firstChild, 2, endContent.firstChild, 2);
+ assert.isFalse(!!element.selectedRange);
+ });
+
+ test('starts in comment thread element', () => {
+ const startContent = stubContent(140, Side.LEFT);
+ if (!startContent?.parentElement) {
+ assert.fail('parent el of start content not found');
+ }
+ const comment =
+ startContent.parentElement.querySelector('.comment-thread');
+ if (!comment?.firstChild) {
+ assert.fail('first child of comment not found');
+ }
+ const endContent = stubContent(141, Side.LEFT);
+ if (!endContent?.firstChild) {
+ assert.fail('first child of end content not found');
+ }
+ emulateSelection(comment.firstChild, 2, endContent.firstChild, 4);
+ if (!element.selectedRange) assert.fail('no range selected');
+ const {range, side} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 140,
+ start_character: 83,
+ end_line: 141,
+ end_character: 4,
+ });
+ assert.equal(side, Side.LEFT);
+ });
+
+ test('ends in comment thread element', () => {
+ const content = stubContent(140, Side.LEFT);
+ if (!content?.firstChild) {
+ assert.fail('first child of content not found');
+ }
+ if (!content?.parentElement) {
+ assert.fail('parent element of content not found');
+ }
+ const comment = content.parentElement.querySelector('.comment-thread');
+ if (!comment?.firstChild) {
+ assert.fail('first child of comment element not found');
+ }
+ emulateSelection(content.firstChild, 4, comment.firstChild, 1);
+ if (!element.selectedRange) assert.fail('no range selected');
+ const {range, side} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 140,
+ start_character: 4,
+ end_line: 140,
+ end_character: 83,
+ });
+ assert.equal(side, Side.LEFT);
+ });
+
+ test('starts in context element', () => {
+ const contextControl = diff
+ .querySelector('.contextControl')!
+ .querySelector('gr-button');
+ if (!contextControl) assert.fail('context control not found');
+ const content = stubContent(146, Side.RIGHT);
+ if (!content) assert.fail('content not found');
+ if (!content.firstChild) assert.fail('content child not found');
+ emulateSelection(contextControl, 0, content.firstChild, 7);
+ // TODO (viktard): Select nearest line.
+ assert.isFalse(!!element.selectedRange);
+ });
+
+ test('ends in context element', () => {
+ const contextControl = diff
+ .querySelector('.contextControl')!
+ .querySelector('gr-button');
+ if (!contextControl) {
+ assert.fail('context control element not found');
+ }
+ const content = stubContent(141, Side.LEFT);
+ if (!content?.firstChild) {
+ assert.fail('first child of content element not found');
+ }
+ emulateSelection(content.firstChild, 2, contextControl, 1);
+ // TODO (viktard): Select nearest line.
+ assert.isFalse(!!element.selectedRange);
+ });
+
+ test('selection containing context element', () => {
+ const startContent = stubContent(130, Side.RIGHT);
+ const endContent = stubContent(146, Side.RIGHT);
+ if (!startContent?.firstChild) {
+ assert.fail('first child of start content not found');
+ }
+ if (!endContent?.firstChild) {
+ assert.fail('first child of end content not found');
+ }
+ emulateSelection(startContent.firstChild, 3, endContent.firstChild, 14);
+ if (!element.selectedRange) assert.fail('no range selected');
+ const {range, side} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 130,
+ start_character: 3,
+ end_line: 146,
+ end_character: 14,
+ });
+ assert.equal(side, Side.RIGHT);
+ });
+
+ test('ends at a tab', () => {
+ const content = stubContent(140, Side.LEFT);
+ if (!content?.firstChild) {
+ assert.fail('first child of content element not found');
+ }
+ const span = content.querySelector('span');
+ if (!span) assert.fail('span element not found');
+ emulateSelection(content.firstChild, 1, span, 0);
+ if (!element.selectedRange) assert.fail('no range selected');
+ const {range, side} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 140,
+ start_character: 1,
+ end_line: 140,
+ end_character: 51,
+ });
+ assert.equal(side, Side.LEFT);
+ });
+
+ test('starts at a tab', () => {
+ const content = stubContent(140, Side.LEFT);
+ if (!content) assert.fail('content element not found');
+ emulateSelection(
+ content.querySelectorAll('hl')[3],
+ 0,
+ content.querySelectorAll('span')[1].nextSibling!,
+ 1
+ );
+ if (!element.selectedRange) assert.fail('no range selected');
+ const {range, side} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 140,
+ start_character: 51,
+ end_line: 140,
+ end_character: 71,
+ });
+ assert.equal(side, Side.LEFT);
+ });
+
+ test('properly accounts for syntax highlighting', () => {
+ const content = stubContent(140, Side.LEFT);
+ if (!content) assert.fail('content element not found');
+ emulateSelection(
+ content.querySelectorAll('hl')[3],
+ 0,
+ content.querySelectorAll('span')[1],
+ 0
+ );
+ if (!element.selectedRange) assert.fail('no range selected');
+ const {range, side} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 140,
+ start_character: 51,
+ end_line: 140,
+ end_character: 69,
+ });
+ assert.equal(side, Side.LEFT);
+ });
+
+ test('GrRangeNormalizer.getTextOffset computes text offset', () => {
+ let content = stubContent(140, Side.LEFT);
+ if (!content) assert.fail('content element not found');
+ if (!content.lastChild) assert.fail('last child of content not found');
+ let child = content.lastChild.lastChild;
+ if (!child) assert.fail('last child of last child of content not found');
+ let result = _getTextOffset(content, child);
+ assert.equal(result, 75);
+ content = stubContent(146, Side.RIGHT);
+ if (!content) assert.fail('content element not found');
+ child = content.lastChild;
+ if (!child) assert.fail('child element not found');
+ result = _getTextOffset(content, child);
+ assert.equal(result, 0);
+ });
+
+ test('fixTripleClickSelection', () => {
+ const startContent = stubContent(119, Side.RIGHT);
+ const endContent = stubContent(120, Side.RIGHT);
+ if (!startContent?.firstChild) {
+ assert.fail('first child of start content not found');
+ }
+ if (!endContent) assert.fail('end content not found');
+ if (!endContent.firstChild) assert.fail('first child not found');
+ emulateSelection(startContent.firstChild, 0, endContent.firstChild, 0);
+ if (!element.selectedRange) assert.fail('no range selected');
+ const {range, side} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 119,
+ start_character: 0,
+ end_line: 119,
+ end_character: element.getLength(startContent),
+ });
+ assert.equal(side, Side.RIGHT);
+ });
+ });
+});
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor.ts b/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor.ts
index 605d493..4a1e5be 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor.ts
@@ -3,7 +3,6 @@
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
-import {PolymerElement} from '@polymer/polymer/polymer-element';
import {
GrDiffLine,
GrDiffLineType,
@@ -17,11 +16,11 @@
hideInContextControl,
} from '../gr-diff/gr-diff-group';
import {CancelablePromise, util} from '../../../scripts/util';
-import {customElement, property} from '@polymer/decorators';
import {DiffContent} from '../../../types/diff';
import {Side} from '../../../constants/constants';
import {debounce, DelayedTask} from '../../../utils/async-util';
import {RenderPreferences} from '../../../api/diff';
+import {assertIsDefined} from '../../../utils/common-util';
const WHOLE_FILE = -1;
@@ -57,6 +56,12 @@
return asyncThreshold * 2;
}
+/** Interface for listening to the output of the processor. */
+export interface GroupConsumer {
+ addGroup(group: GrDiffGroup): void;
+ clearGroups(): void;
+}
+
/**
* Converts the API's `DiffContent`s to `GrDiffGroup`s for rendering.
*
@@ -82,19 +87,10 @@
* that the part that is within the context or has comments is shown, while
* the rest is not.
*/
-@customElement('gr-diff-processor')
-export class GrDiffProcessor extends PolymerElement {
+export class GrDiffProcessor {
context = 3;
- /**
- * The builder elements watches this (two-way data binding and @observe) and
- * thus passes each added group on to the renderer (i.e. gr-diff-builder).
- * You must only add to this array and not modify it later (only when
- * resetting). The source of truth is then held by gr-diff-builder, which also
- * reflects expanding and collapsing of groups.
- */
- @property({type: Array, notify: true})
- groups: GrDiffGroup[] = [];
+ consumer?: GroupConsumer;
keyLocations: KeyLocations = {left: {}, right: {}};
@@ -109,18 +105,6 @@
private resetIsScrollingTask?: DelayedTask;
- override connectedCallback() {
- super.connectedCallback();
- window.addEventListener('scroll', this.handleWindowScroll);
- }
-
- override disconnectedCallback() {
- this.resetIsScrollingTask?.cancel();
- this.cancel();
- window.removeEventListener('scroll', this.handleWindowScroll);
- super.disconnectedCallback();
- }
-
private readonly handleWindowScroll = () => {
this.isScrolling = true;
this.resetIsScrollingTask = debounce(
@@ -141,10 +125,12 @@
// Cancel any still running process() calls, because they append to the
// same groups field.
this.cancel();
+ window.addEventListener('scroll', this.handleWindowScroll);
- this.groups = [];
- this.push('groups', this.makeGroup('LOST'));
- this.push('groups', this.makeGroup(FILE));
+ assertIsDefined(this.consumer, 'consumer');
+ this.consumer.clearGroups();
+ this.consumer.addGroup(this.makeGroup('LOST'));
+ this.consumer.addGroup(this.makeGroup(FILE));
// If it's a binary diff, we won't be rendering hunks of text differences
// so finish processing.
@@ -178,7 +164,8 @@
// Process the next chunk and incorporate the result.
const stateUpdate = this.processNext(state, chunks);
for (const group of stateUpdate.groups) {
- this.push('groups', group);
+ assertIsDefined(this.consumer, 'consumer');
+ this.consumer.addGroup(group);
currentBatch += group.lines.length;
}
state.lineNums.left += stateUpdate.lineDelta.left;
@@ -199,6 +186,7 @@
);
return this.processPromise.finally(() => {
this.processPromise = null;
+ window.removeEventListener('scroll', this.handleWindowScroll);
});
}
@@ -213,6 +201,7 @@
if (this.processPromise) {
this.processPromise.cancel();
}
+ window.removeEventListener('scroll', this.handleWindowScroll);
}
/**
@@ -746,9 +735,3 @@
}
}
}
-
-declare global {
- interface HTMLElementTagNameMap {
- 'gr-diff-processor': GrDiffProcessor;
- }
-}
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor_test.ts
index ba59af0..60a1cba 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor_test.ts
@@ -10,8 +10,6 @@
import {GrDiffProcessor, State} from './gr-diff-processor';
import {DiffContent} from '../../../types/diff';
-const basicFixture = fixtureFromElement('gr-diff-processor');
-
suite('gr-diff-processor tests', () => {
const WHOLE_FILE = -1;
const loremIpsum =
@@ -22,13 +20,22 @@
'fugit assum per.';
let element: GrDiffProcessor;
+ let groups: GrDiffGroup[];
setup(() => {});
suite('not logged in', () => {
setup(() => {
- element = basicFixture.instantiate();
-
+ groups = [];
+ element = new GrDiffProcessor();
+ element.consumer = {
+ addGroup(group: GrDiffGroup) {
+ groups.push(group);
+ },
+ clearGroups() {
+ groups = [];
+ },
+ };
element.context = 4;
});
@@ -51,7 +58,6 @@
];
return element.process(content, false).then(() => {
- const groups = element.groups;
groups.shift(); // remove portedThreadsWithoutRangeGroup
assert.equal(groups.length, 4);
@@ -113,7 +119,6 @@
const content = [{b: ['foo']}];
return element.process(content, false).then(() => {
- const groups = element.groups;
groups.shift(); // remove portedThreadsWithoutRangeGroup
assert.equal(groups[0].type, GrDiffGroupType.BOTH);
@@ -137,7 +142,6 @@
];
return element.process(content, false).then(() => {
- const groups = element.groups;
groups.shift(); // remove portedThreadsWithoutRangeGroup
// group[0] is the file group
@@ -168,7 +172,6 @@
await element.process(content, false);
- const groups = element.groups;
groups.shift(); // remove portedThreadsWithoutRangeGroup
// group[0] is the file group
@@ -214,7 +217,6 @@
];
return element.process(content, false).then(() => {
- const groups = element.groups;
groups.shift(); // remove portedThreadsWithoutRangeGroup
// group[0] is the file group
@@ -239,7 +241,6 @@
];
return element.process(content, false).then(() => {
- const groups = element.groups;
groups.shift(); // remove portedThreadsWithoutRangeGroup
// group[0] is the file group
@@ -268,7 +269,6 @@
];
return element.process(content, false).then(() => {
- const groups = element.groups;
groups.shift(); // remove portedThreadsWithoutRangeGroup
// group[0] is the file group
@@ -306,7 +306,6 @@
];
return element.process(content, false).then(() => {
- const groups = element.groups;
groups.shift(); // remove portedThreadsWithoutRangeGroup
// group[0] is the file group
@@ -395,7 +394,6 @@
];
return element.process(content, false).then(() => {
- const groups = element.groups;
groups.shift(); // remove portedThreadsWithoutRangeGroup
// group[0] is the file group
@@ -431,7 +429,6 @@
];
return element.process(content, false).then(() => {
- const groups = element.groups;
groups.shift(); // remove portedThreadsWithoutRangeGroup
// group[0] is the file group
@@ -458,7 +455,6 @@
await element.process(content, false);
- const groups = element.groups;
groups.shift(); // remove portedThreadsWithoutRangeGroup
// group[0] is the file group
@@ -734,21 +730,21 @@
element.isScrolling = true;
element.process(content, false);
// Just the files group - no more processing during scrolling.
- assert.equal(element.groups.length, 2);
+ assert.equal(groups.length, 2);
element.isScrolling = false;
element.process(content, false);
// More groups have been processed. How many does not matter here.
- assert.isAtLeast(element.groups.length, 3);
+ assert.isAtLeast(groups.length, 3);
});
test('image diffs', () => {
const content = Array(200).fill({ab: ['', '']});
element.process(content, true);
- assert.equal(element.groups.length, 2);
+ assert.equal(groups.length, 2);
// Image diffs don't process content, just the 'FILE' line.
- assert.equal(element.groups[0].lines.length, 1);
+ assert.equal(groups[0].lines.length, 1);
});
suite('processNext', () => {
@@ -1074,11 +1070,4 @@
});
});
});
-
- test('detaching cancels', () => {
- element = basicFixture.instantiate();
- const cancelStub = sinon.stub(element, 'cancel');
- element.disconnectedCallback();
- assert(cancelStub.called);
- });
});
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection.ts b/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection.ts
index 2665ef0..4bb8cc3 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection.ts
@@ -1,39 +1,23 @@
/**
* @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 '../../../styles/shared-styles';
-import {addListener} from '@polymer/polymer/lib/utils/gestures';
-import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-diff-selection_html';
import {
normalize,
NormalizedRange,
} from '../gr-diff-highlight/gr-range-normalizer';
import {descendedFromClass, querySelectorAll} from '../../../utils/dom-util';
-import {customElement, property, observe} from '@polymer/decorators';
import {DiffInfo} from '../../../types/diff';
import {Side} from '../../../constants/constants';
-import {GrDiffBuilderElement} from '../gr-diff-builder/gr-diff-builder-element';
import {
getLineElByChild,
getSide,
getSideByLineEl,
isThreadEl,
} from '../gr-diff/gr-diff-utils';
+import {assertIsDefined} from '../../../utils/common-util';
/**
* Possible CSS classes indicating the state of selection. Dynamically added/
@@ -55,49 +39,35 @@
return {left: null, right: null};
}
-@customElement('gr-diff-selection')
-export class GrDiffSelection extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
-
- @property({type: Object})
+export class GrDiffSelection {
+ // visible for testing
diff?: DiffInfo;
- @property({type: Object})
- _cachedDiffBuilder?: GrDiffBuilderElement;
+ // visible for testing
+ diffTable?: HTMLElement;
- @property({type: Object})
- _linesCache: LinesCache = {left: null, right: null};
+ // visible for testing
+ linesCache: LinesCache = getNewCache();
- constructor() {
- super();
- this.addEventListener('copy', e => this._handleCopy(e));
- addListener(this, 'down', e => this._handleDown(e));
+ init(diff: DiffInfo, diffTable: HTMLElement) {
+ this.cleanup();
+ this.diff = diff;
+ this.diffTable = diffTable;
+ this.diffTable.classList.add(SelectionClass.RIGHT);
+ this.diffTable.addEventListener('copy', this.handleCopy);
+ this.diffTable.addEventListener('mousedown', this.handleDown);
+ this.linesCache = getNewCache();
}
- override connectedCallback() {
- super.connectedCallback();
- this.classList.add(SelectionClass.RIGHT);
+ cleanup() {
+ if (!this.diffTable) return;
+ this.diffTable.removeEventListener('copy', this.handleCopy);
+ this.diffTable.removeEventListener('mousedown', this.handleDown);
}
- get diffBuilder() {
- if (!this._cachedDiffBuilder) {
- this._cachedDiffBuilder = this.querySelector(
- 'gr-diff-builder'
- ) as GrDiffBuilderElement;
- }
- return this._cachedDiffBuilder;
- }
-
- @observe('diff')
- _diffChanged() {
- this._linesCache = getNewCache();
- }
-
- _handleDownOnRangeComment(node: Element) {
+ handleDownOnRangeComment(node: Element) {
if (isThreadEl(node)) {
- this._setClasses([
+ this.setClasses([
SelectionClass.COMMENT,
getSide(node) === Side.LEFT
? SelectionClass.LEFT
@@ -108,14 +78,13 @@
return false;
}
- _handleDown(e: Event) {
+ handleDown = (e: Event) => {
const target = e.target;
if (!(target instanceof Element)) return;
- // Handle the down event on comment thread in Polymer 2
- const handled = this._handleDownOnRangeComment(target);
+ const handled = this.handleDownOnRangeComment(target);
if (handled) return;
const lineEl = getLineElByChild(target);
- const blameSelected = this._elementDescendedFromClass(target, 'blame');
+ const blameSelected = descendedFromClass(target, 'blame', this.diffTable);
if (!lineEl && !blameSelected) {
return;
}
@@ -125,9 +94,10 @@
if (blameSelected) {
targetClasses.push(SelectionClass.BLAME);
} else if (lineEl) {
- const commentSelected = this._elementDescendedFromClass(
+ const commentSelected = descendedFromClass(
target,
- 'gr-comment'
+ 'gr-comment',
+ this.diffTable
);
const side = getSideByLineEl(lineEl);
@@ -140,60 +110,50 @@
}
}
- this._setClasses(targetClasses);
- }
+ this.setClasses(targetClasses);
+ };
/**
* Set the provided list of classes on the element, to the exclusion of all
* other SelectionClass values.
*/
- _setClasses(targetClasses: string[]) {
+ setClasses(targetClasses: string[]) {
+ if (!this.diffTable) return;
// Remove any selection classes that do not belong.
for (const className of Object.values(SelectionClass)) {
if (!targetClasses.includes(className)) {
- this.classList.remove(className);
+ this.diffTable.classList.remove(className);
}
}
// Add new selection classes iff they are not already present.
- for (const _class of targetClasses) {
- if (!this.classList.contains(_class)) {
- this.classList.add(_class);
+ for (const targetClass of targetClasses) {
+ if (!this.diffTable.classList.contains(targetClass)) {
+ this.diffTable.classList.add(targetClass);
}
}
}
- _getCopyEventTarget(e: Event) {
- return (dom(e) as EventApi).rootTarget;
- }
-
- /**
- * Utility function to determine whether an element is a descendant of
- * another element with the particular className.
- */
- _elementDescendedFromClass(element: Element, className: string) {
- return descendedFromClass(element, className, this.diffBuilder.diffElement);
- }
-
- _handleCopy(e: ClipboardEvent) {
+ handleCopy = (e: ClipboardEvent) => {
let commentSelected = false;
- const target = this._getCopyEventTarget(e);
+ const target = e.composedPath()[0];
if (!(target instanceof Element)) return;
if (target instanceof HTMLTextAreaElement) return;
- if (!this._elementDescendedFromClass(target, 'diff-row')) return;
- if (this.classList.contains(SelectionClass.COMMENT)) {
+ if (!descendedFromClass(target, 'diff-row', this.diffTable)) return;
+ if (!this.diffTable) return;
+ if (this.diffTable.classList.contains(SelectionClass.COMMENT)) {
commentSelected = true;
}
const lineEl = getLineElByChild(target);
if (!lineEl) return;
const side = getSideByLineEl(lineEl);
- const text = this._getSelectedText(side, commentSelected);
+ const text = this.getSelectedText(side, commentSelected);
if (text && e.clipboardData) {
e.clipboardData.setData('Text', text);
e.preventDefault();
}
- }
+ };
- _getSelection() {
+ getSelection() {
const diffHosts = querySelectorAll(document.body, 'gr-diff');
if (!diffHosts.length) return document.getSelection();
@@ -219,13 +179,13 @@
* @param commentSelected Whether or not a comment is selected.
* @return The selected text.
*/
- _getSelectedText(side: Side, commentSelected: boolean) {
- const sel = this._getSelection();
+ getSelectedText(side: Side, commentSelected: boolean) {
+ const sel = this.getSelection();
if (!sel || sel.rangeCount !== 1) {
return ''; // No multi-select support yet.
}
if (commentSelected) {
- return this._getCommentLines(sel, side);
+ return this.getCommentLines(sel, side);
}
const range = normalize(sel.getRangeAt(0));
const startLineEl = getLineElByChild(range.startContainer);
@@ -250,7 +210,7 @@
if (endLineDataValue) endLineNum = Number(endLineDataValue);
}
- return this._getRangeFromDiff(
+ return this.getRangeFromDiff(
startLineNum,
range.startOffset,
endLineNum,
@@ -262,7 +222,7 @@
/**
* Query the diff object for the selected lines.
*/
- _getRangeFromDiff(
+ getRangeFromDiff(
startLineNum: number,
startOffset: number,
endLineNum: number | undefined,
@@ -274,7 +234,7 @@
startLineNum -= skipChunk.skip!;
if (endLineNum) endLineNum -= skipChunk.skip!;
}
- const lines = this._getDiffLines(side).slice(startLineNum - 1, endLineNum);
+ const lines = this.getDiffLines(side).slice(startLineNum - 1, endLineNum);
if (lines.length) {
lines[lines.length - 1] = lines[lines.length - 1].substring(0, endOffset);
lines[0] = lines[0].substring(startOffset);
@@ -288,9 +248,9 @@
* @param side The side that is currently selected.
* @return An array of strings indexed by line number.
*/
- _getDiffLines(side: Side): string[] {
- if (this._linesCache[side]) {
- return this._linesCache[side]!;
+ getDiffLines(side: Side): string[] {
+ if (this.linesCache[side]) {
+ return this.linesCache[side]!;
}
if (!this.diff) return [];
let lines: string[] = [];
@@ -303,7 +263,7 @@
lines = lines.concat(chunk.b);
}
}
- this._linesCache[side] = lines;
+ this.linesCache[side] = lines;
return lines;
}
@@ -315,11 +275,11 @@
* @param side The side that is currently selected.
* @return The selected comment text.
*/
- _getCommentLines(sel: Selection, side: Side) {
+ getCommentLines(sel: Selection, side: Side) {
const range = normalize(sel.getRangeAt(0));
const content = [];
- // Query the diffElement for comments.
- const messages = this.diffBuilder.diffElement.querySelectorAll(
+ assertIsDefined(this.diffTable, 'diffTable');
+ const messages = this.diffTable.querySelectorAll(
`.side-by-side [data-side="${side}"] .message *, .unified .message *`
);
@@ -339,9 +299,9 @@
if (
el.id === 'output' &&
- !this._elementDescendedFromClass(el, 'collapsed')
+ !descendedFromClass(el, 'collapsed', this.diffTable)
) {
- content.push(this._getTextContentForRange(el, sel, range));
+ content.push(this.getTextContentForRange(el, sel, range));
}
}
}
@@ -359,7 +319,7 @@
* @param range The normalized selection range.
* @return The text within the selection.
*/
- _getTextContentForRange(
+ getTextContentForRange(
domNode: Node,
sel: Selection,
range: NormalizedRange
@@ -379,15 +339,9 @@
}
} else {
for (const childNode of domNode.childNodes) {
- text += this._getTextContentForRange(childNode, sel, range);
+ text += this.getTextContentForRange(childNode, sel, range);
}
}
return text;
}
}
-
-declare global {
- interface HTMLElementTagNameMap {
- 'gr-diff-selection': GrDiffSelection;
- }
-}
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection_html.ts b/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection_html.ts
deleted file mode 100644
index bd0e034..0000000
--- a/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection_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-selection/gr-diff-selection_test.js b/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection_test.js
deleted file mode 100644
index 15454f9..0000000
--- a/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection_test.js
+++ /dev/null
@@ -1,389 +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-selection.js';
-import {html} from '@polymer/polymer/lib/utils/html-tag.js';
-
-// Splitting long lines in html into shorter rows breaks tests:
-// zero-length text nodes and new lines are not expected in some places
-/* eslint-disable max-len */
-const basicFixture = fixtureFromTemplate(html`
-<gr-diff-selection>
- <table id="diffTable" class="side-by-side">
- <tr class="diff-row">
- <td class="blame" data-line-number="1"></td>
- <td class="lineNum left" data-value="1">1</td>
- <td class="content">
- <div class="contentText" data-side="left">ba ba</div>
- <div data-side="left">
- <div class="comment-thread">
- <div class="gr-formatted-text message">
- <span id="output" class="gr-linked-text">This is a comment</span>
- </div>
- </div>
- </div>
- </td>
- <td class="lineNum right" data-value="1">1</td>
- <td class="content">
- <div class="contentText" data-side="right">some other text</div>
- </td>
- </tr>
- <tr class="diff-row">
- <td class="blame" data-line-number="2"></td>
- <td class="lineNum left" data-value="2">2</td>
- <td class="content">
- <div class="contentText" data-side="left">zin</div>
- </td>
- <td class="lineNum right" data-value="2">2</td>
- <td class="content">
- <div class="contentText" data-side="right">more more more</div>
- <div data-side="right">
- <div class="comment-thread">
- <div class="gr-formatted-text message">
- <span id="output" class="gr-linked-text">This is a comment on the right</span>
- </div>
- </div>
- </div>
- </td>
- </tr>
- <tr class="diff-row">
- <td class="blame" data-line-number="3"></td>
- <td class="lineNum left" data-value="3">3</td>
- <td class="content">
- <div class="contentText" data-side="left">ga ga</div>
- <div data-side="left">
- <div class="comment-thread">
- <div class="gr-formatted-text message">
- <span id="output" class="gr-linked-text">This is <a>a</a> different comment 💩 unicode is fun</span>
- </div>
- </div>
- </div>
- </td>
- <td class="lineNum right" data-value="3">3</td>
- </tr>
- <tr class="diff-row">
- <td class="blame" data-line-number="4"></td>
- <td class="lineNum left" data-value="4">4</td>
- <td class="content">
- <div class="contentText" data-side="left">ga ga</div>
- <div data-side="left">
- <div class="comment-thread">
- <textarea data-side="right">test for textarea copying</textarea>
- </div>
- </div>
- </td>
- <td class="lineNum right" data-value="4">4</td>
- </tr>
- <tr class="not-diff-row">
- <td class="other">
- <div class="contentText" data-side="right">some other text</div>
- </td>
- </tr>
- </table>
- </gr-diff-selection>
-`);
-/* eslint-enable max-len */
-
-suite('gr-diff-selection', () => {
- let element;
-
- const emulateCopyOn = function(target) {
- const fakeEvent = {
- target,
- preventDefault: sinon.stub(),
- clipboardData: {
- setData: sinon.stub(),
- },
- };
- element._getCopyEventTarget.returns(target);
- element._handleCopy(fakeEvent);
- return fakeEvent;
- };
-
- setup(() => {
- element = basicFixture.instantiate();
-
- sinon.stub(element, '_getCopyEventTarget');
- element._cachedDiffBuilder = {
- getLineElByChild: sinon.stub().returns({}),
- getSideByLineEl: sinon.stub(),
- diffElement: element.querySelector('#diffTable'),
- };
- element.diff = {
- content: [
- {
- a: ['ba ba'],
- b: ['some other text'],
- },
- {
- a: ['zin'],
- b: ['more more more'],
- },
- {
- a: ['ga ga'],
- b: ['some other text'],
- },
- ],
- };
- });
-
- test('applies selected-left on left side click', () => {
- element.classList.add('selected-right');
- const lineNumberEl = element.querySelector('.lineNum.left');
- MockInteractions.down(lineNumberEl);
- assert.isTrue(
- element.classList.contains('selected-left'), 'adds selected-left');
- assert.isFalse(
- element.classList.contains('selected-right'),
- 'removes selected-right');
- });
-
- test('applies selected-right on right side click', () => {
- element.classList.add('selected-left');
- const lineNumberEl = element.querySelector('.lineNum.right');
- MockInteractions.down(lineNumberEl);
- assert.isTrue(
- element.classList.contains('selected-right'), 'adds selected-right');
- assert.isFalse(
- element.classList.contains('selected-left'), 'removes selected-left');
- });
-
- test('applies selected-blame on blame click', () => {
- element.classList.add('selected-left');
- element.diffBuilder.getLineElByChild.returns(null);
- sinon.stub(element, '_elementDescendedFromClass').callsFake(
- (el, className) => className === 'blame');
- MockInteractions.down(element);
- assert.isTrue(
- element.classList.contains('selected-blame'), 'adds selected-right');
- assert.isFalse(
- element.classList.contains('selected-left'), 'removes selected-left');
- });
-
- test('ignores copy for non-content Element', () => {
- sinon.stub(element, '_getSelectedText');
- emulateCopyOn(element.querySelector('.not-diff-row'));
- assert.isFalse(element._getSelectedText.called);
- });
-
- test('asks for text for left side Elements', () => {
- element._cachedDiffBuilder.getSideByLineEl.returns('left');
- sinon.stub(element, '_getSelectedText');
- emulateCopyOn(element.querySelector('div.contentText'));
- assert.deepEqual(['left', false], element._getSelectedText.lastCall.args);
- });
-
- test('reacts to copy for content Elements', () => {
- sinon.stub(element, '_getSelectedText');
- emulateCopyOn(element.querySelector('div.contentText'));
- assert.isTrue(element._getSelectedText.called);
- });
-
- test('copy event is prevented for content Elements', () => {
- sinon.stub(element, '_getSelectedText');
- element._cachedDiffBuilder.getSideByLineEl.returns('left');
- element._getSelectedText.returns('test');
- const event = emulateCopyOn(element.querySelector('div.contentText'));
- assert.isTrue(event.preventDefault.called);
- });
-
- test('inserts text into clipboard on copy', () => {
- sinon.stub(element, '_getSelectedText').returns('the text');
- const event = emulateCopyOn(element.querySelector('div.contentText'));
- assert.deepEqual(
- ['Text', 'the text'], event.clipboardData.setData.lastCall.args);
- });
-
- test('_setClasses adds given SelectionClass values, removes others', () => {
- element.classList.add('selected-right');
- element._setClasses(['selected-comment', 'selected-left']);
- assert.isTrue(element.classList.contains('selected-comment'));
- assert.isTrue(element.classList.contains('selected-left'));
- assert.isFalse(element.classList.contains('selected-right'));
- assert.isFalse(element.classList.contains('selected-blame'));
-
- element._setClasses(['selected-blame']);
- assert.isFalse(element.classList.contains('selected-comment'));
- assert.isFalse(element.classList.contains('selected-left'));
- assert.isFalse(element.classList.contains('selected-right'));
- assert.isTrue(element.classList.contains('selected-blame'));
- });
-
- test('_setClasses removes before it ads', () => {
- element.classList.add('selected-right');
- const addStub = sinon.stub(element.classList, 'add');
- const removeStub = sinon.stub(element.classList, 'remove').callsFake(
- () => {
- assert.isFalse(addStub.called);
- });
- element._setClasses(['selected-comment', 'selected-left']);
- assert.isTrue(addStub.called);
- assert.isTrue(removeStub.called);
- });
-
- test('copies content correctly', () => {
- // Fetch the line number.
- element._cachedDiffBuilder.getLineElByChild = function(child) {
- while (!child.classList.contains('content') && child.parentElement) {
- child = child.parentElement;
- }
- return child.previousElementSibling;
- };
-
- element.classList.add('selected-left');
- element.classList.remove('selected-right');
-
- const selection = document.getSelection();
- selection.removeAllRanges();
- const range = document.createRange();
- range.setStart(element.querySelector('div.contentText').firstChild, 3);
- range.setEnd(
- element.querySelectorAll('div.contentText')[4].firstChild, 2);
- selection.addRange(range);
- assert.equal(element._getSelectedText('left'), 'ba\nzin\nga');
- });
-
- test('copies comments', () => {
- element.classList.add('selected-left');
- element.classList.add('selected-comment');
- element.classList.remove('selected-right');
- const selection = document.getSelection();
- selection.removeAllRanges();
- const range = document.createRange();
- range.setStart(
- element.querySelector('.gr-formatted-text *').firstChild, 3);
- range.setEnd(
- element.querySelectorAll('.gr-formatted-text *')[2].childNodes[2], 7);
- selection.addRange(range);
- assert.equal('s is a comment\nThis is a differ',
- element._getSelectedText('left', true));
- });
-
- test('respects astral chars in comments', () => {
- element.classList.add('selected-left');
- element.classList.add('selected-comment');
- element.classList.remove('selected-right');
- const selection = document.getSelection();
- selection.removeAllRanges();
- const range = document.createRange();
- const nodes = element.querySelectorAll('.gr-formatted-text *');
- range.setStart(nodes[2].childNodes[2], 13);
- range.setEnd(nodes[2].childNodes[2], 23);
- selection.addRange(range);
- assert.equal('mment 💩 u',
- element._getSelectedText('left', true));
- });
-
- test('defers to default behavior for textarea', () => {
- element.classList.add('selected-left');
- element.classList.remove('selected-right');
- const selectedTextSpy = sinon.spy(element, '_getSelectedText');
- emulateCopyOn(element.querySelector('textarea'));
- assert.isFalse(selectedTextSpy.called);
- });
-
- test('regression test for 4794', () => {
- element._cachedDiffBuilder.getLineElByChild = function(child) {
- while (!child.classList.contains('content') && child.parentElement) {
- child = child.parentElement;
- }
- return child.previousElementSibling;
- };
-
- element.classList.add('selected-right');
- element.classList.remove('selected-left');
-
- const selection = document.getSelection();
- selection.removeAllRanges();
- const range = document.createRange();
- range.setStart(
- element.querySelectorAll('div.contentText')[1].firstChild, 4);
- range.setEnd(
- element.querySelectorAll('div.contentText')[1].firstChild, 10);
- selection.addRange(range);
- assert.equal(element._getSelectedText('right'), ' other');
- });
-
- test('copies to end of side (issue 7895)', () => {
- element._cachedDiffBuilder.getLineElByChild = function(child) {
- // Return null for the end container.
- if (child.textContent === 'ga ga') { return null; }
- while (!child.classList.contains('content') && child.parentElement) {
- child = child.parentElement;
- }
- return child.previousElementSibling;
- };
- element.classList.add('selected-left');
- element.classList.remove('selected-right');
- const selection = document.getSelection();
- selection.removeAllRanges();
- const range = document.createRange();
- range.setStart(element.querySelector('div.contentText').firstChild, 3);
- range.setEnd(
- element.querySelectorAll('div.contentText')[4].firstChild, 2);
- selection.addRange(range);
- assert.equal(element._getSelectedText('left'), 'ba\nzin\nga');
- });
-
- suite('_getTextContentForRange', () => {
- let selection;
- let range;
- let nodes;
-
- setup(() => {
- element.classList.add('selected-left');
- element.classList.add('selected-comment');
- element.classList.remove('selected-right');
- selection = document.getSelection();
- selection.removeAllRanges();
- range = document.createRange();
- nodes = element.querySelectorAll('.gr-formatted-text *');
- });
-
- test('multi level element contained in range', () => {
- range.setStart(nodes[2].childNodes[0], 1);
- range.setEnd(nodes[2].childNodes[2], 7);
- selection.addRange(range);
- assert.equal(element._getTextContentForRange(element, selection, range),
- 'his is a differ');
- });
-
- test('multi level element as startContainer of range', () => {
- range.setStart(nodes[2].childNodes[1], 0);
- range.setEnd(nodes[2].childNodes[2], 7);
- selection.addRange(range);
- assert.equal(element._getTextContentForRange(element, selection, range),
- 'a differ');
- });
-
- test('startContainer === endContainer', () => {
- range.setStart(nodes[0].firstChild, 2);
- range.setEnd(nodes[0].firstChild, 12);
- selection.addRange(range);
- assert.equal(element._getTextContentForRange(element, selection, range),
- 'is is a co');
- });
- });
-
- test('cache is reset when diff changes', () => {
- element._linesCache = {left: 'test', right: 'test'};
- element.diff = {};
- flush();
- assert.deepEqual(element._linesCache, {left: null, right: null});
- });
-});
-
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection_test.ts
new file mode 100644
index 0000000..b44114a
--- /dev/null
+++ b/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection_test.ts
@@ -0,0 +1,390 @@
+/**
+ * @license
+ * Copyright 2016 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup-karma';
+import './gr-diff-selection';
+import {GrDiffSelection} from './gr-diff-selection';
+import {createDiff} from '../../../test/test-data-generators';
+import {DiffInfo, Side} from '../../../api/diff';
+import {GrFormattedText} from '../../../elements/shared/gr-formatted-text/gr-formatted-text';
+import {fixture, html} from '@open-wc/testing-helpers';
+import {mouseDown} from '../../../test/test-utils';
+
+const diffTableTemplate = html`
+ <table id="diffTable" class="side-by-side">
+ <tr class="diff-row">
+ <td class="blame" data-line-number="1"></td>
+ <td class="lineNum left" data-value="1">1</td>
+ <td class="content">
+ <div class="contentText" data-side="left">ba ba</div>
+ <div data-side="left">
+ <div class="comment-thread">
+ <div class="gr-formatted-text message">
+ <span id="output" class="gr-linked-text">This is a comment</span>
+ </div>
+ </div>
+ </div>
+ </td>
+ <td class="lineNum right" data-value="1">1</td>
+ <td class="content">
+ <div class="contentText" data-side="right">some other text</div>
+ </td>
+ </tr>
+ <tr class="diff-row">
+ <td class="blame" data-line-number="2"></td>
+ <td class="lineNum left" data-value="2">2</td>
+ <td class="content">
+ <div class="contentText" data-side="left">zin</div>
+ </td>
+ <td class="lineNum right" data-value="2">2</td>
+ <td class="content">
+ <div class="contentText" data-side="right">more more more</div>
+ <div data-side="right">
+ <div class="comment-thread">
+ <div class="gr-formatted-text message">
+ <span id="output" class="gr-linked-text"
+ >This is a comment on the right</span
+ >
+ </div>
+ </div>
+ </div>
+ </td>
+ </tr>
+ <tr class="diff-row">
+ <td class="blame" data-line-number="3"></td>
+ <td class="lineNum left" data-value="3">3</td>
+ <td class="content">
+ <div class="contentText" data-side="left">ga ga</div>
+ <div data-side="left">
+ <div class="comment-thread">
+ <div class="gr-formatted-text message">
+ <span id="output" class="gr-linked-text"
+ >This is <a>a</a> different comment 💩 unicode is fun</span
+ >
+ </div>
+ </div>
+ </div>
+ </td>
+ <td class="lineNum right" data-value="3">3</td>
+ </tr>
+ <tr class="diff-row">
+ <td class="blame" data-line-number="4"></td>
+ <td class="lineNum left" data-value="4">4</td>
+ <td class="content">
+ <div class="contentText" data-side="left">ga ga</div>
+ <div data-side="left">
+ <div class="comment-thread">
+ <textarea data-side="right">test for textarea copying</textarea>
+ </div>
+ </div>
+ </td>
+ <td class="lineNum right" data-value="4">4</td>
+ </tr>
+ <tr class="not-diff-row">
+ <td class="other">
+ <div class="contentText" data-side="right">some other text</div>
+ </td>
+ </tr>
+ </table>
+`;
+
+suite('gr-diff-selection', () => {
+ let element: GrDiffSelection;
+ let diffTable: HTMLTableElement;
+
+ const emulateCopyOn = function (target: HTMLElement | null) {
+ const fakeEvent = {
+ target,
+ preventDefault: sinon.stub(),
+ composedPath() {
+ return [target];
+ },
+ clipboardData: {
+ setData: sinon.stub(),
+ },
+ };
+ element.handleCopy(fakeEvent as unknown as ClipboardEvent);
+ return fakeEvent;
+ };
+
+ setup(async () => {
+ element = new GrDiffSelection();
+ diffTable = await fixture<HTMLTableElement>(diffTableTemplate);
+
+ const diff: DiffInfo = {
+ ...createDiff(),
+ content: [
+ {
+ a: ['ba ba'],
+ b: ['some other text'],
+ },
+ {
+ a: ['zin'],
+ b: ['more more more'],
+ },
+ {
+ a: ['ga ga'],
+ b: ['some other text'],
+ },
+ ],
+ };
+ element.init(diff, diffTable);
+ });
+
+ test('applies selected-left on left side click', () => {
+ element.diffTable!.classList.add('selected-right');
+ const lineNumberEl = diffTable.querySelector<HTMLElement>('.lineNum.left');
+ if (!lineNumberEl) assert.fail('line number element missing');
+ mouseDown(lineNumberEl);
+ assert.isTrue(
+ element.diffTable!.classList.contains('selected-left'),
+ 'adds selected-left'
+ );
+ assert.isFalse(
+ element.diffTable!.classList.contains('selected-right'),
+ 'removes selected-right'
+ );
+ });
+
+ test('applies selected-right on right side click', () => {
+ element.diffTable!.classList.add('selected-left');
+ const lineNumberEl = diffTable.querySelector<HTMLElement>('.lineNum.right');
+ if (!lineNumberEl) assert.fail('line number element missing');
+ mouseDown(lineNumberEl);
+ assert.isTrue(
+ element.diffTable!.classList.contains('selected-right'),
+ 'adds selected-right'
+ );
+ assert.isFalse(
+ element.diffTable!.classList.contains('selected-left'),
+ 'removes selected-left'
+ );
+ });
+
+ test('applies selected-blame on blame click', () => {
+ element.diffTable!.classList.add('selected-left');
+ const blameDiv = document.createElement('div');
+ blameDiv.classList.add('blame');
+ element.diffTable!.appendChild(blameDiv);
+ mouseDown(blameDiv);
+ assert.isTrue(
+ element.diffTable!.classList.contains('selected-blame'),
+ 'adds selected-right'
+ );
+ assert.isFalse(
+ element.diffTable!.classList.contains('selected-left'),
+ 'removes selected-left'
+ );
+ });
+
+ test('ignores copy for non-content Element', () => {
+ const getSelectedTextStub = sinon.stub(element, 'getSelectedText');
+ emulateCopyOn(diffTable.querySelector('.not-diff-row'));
+ assert.isFalse(getSelectedTextStub.called);
+ });
+
+ test('asks for text for left side Elements', () => {
+ const getSelectedTextStub = sinon.stub(element, 'getSelectedText');
+ emulateCopyOn(diffTable.querySelector('div.contentText'));
+ assert.deepEqual([Side.LEFT, false], getSelectedTextStub.lastCall.args);
+ });
+
+ test('reacts to copy for content Elements', () => {
+ const getSelectedTextStub = sinon.stub(element, 'getSelectedText');
+ emulateCopyOn(diffTable.querySelector('div.contentText'));
+ assert.isTrue(getSelectedTextStub.called);
+ });
+
+ test('copy event is prevented for content Elements', () => {
+ const getSelectedTextStub = sinon.stub(element, 'getSelectedText');
+ getSelectedTextStub.returns('test');
+ const event = emulateCopyOn(diffTable.querySelector('div.contentText'));
+ assert.isTrue(event.preventDefault.called);
+ });
+
+ test('inserts text into clipboard on copy', () => {
+ sinon.stub(element, 'getSelectedText').returns('the text');
+ const event = emulateCopyOn(diffTable.querySelector('div.contentText'));
+ assert.deepEqual(
+ ['Text', 'the text'],
+ event.clipboardData.setData.lastCall.args
+ );
+ });
+
+ test('setClasses adds given SelectionClass values, removes others', () => {
+ element.diffTable!.classList.add('selected-right');
+ element.setClasses(['selected-comment', 'selected-left']);
+ assert.isTrue(element.diffTable!.classList.contains('selected-comment'));
+ assert.isTrue(element.diffTable!.classList.contains('selected-left'));
+ assert.isFalse(element.diffTable!.classList.contains('selected-right'));
+ assert.isFalse(element.diffTable!.classList.contains('selected-blame'));
+
+ element.setClasses(['selected-blame']);
+ assert.isFalse(element.diffTable!.classList.contains('selected-comment'));
+ assert.isFalse(element.diffTable!.classList.contains('selected-left'));
+ assert.isFalse(element.diffTable!.classList.contains('selected-right'));
+ assert.isTrue(element.diffTable!.classList.contains('selected-blame'));
+ });
+
+ test('setClasses removes before it ads', () => {
+ element.diffTable!.classList.add('selected-right');
+ const addStub = sinon.stub(element.diffTable!.classList, 'add');
+ const removeStub = sinon
+ .stub(element.diffTable!.classList, 'remove')
+ .callsFake(() => {
+ assert.isFalse(addStub.called);
+ });
+ element.setClasses(['selected-comment', 'selected-left']);
+ assert.isTrue(addStub.called);
+ assert.isTrue(removeStub.called);
+ });
+
+ test('copies content correctly', () => {
+ element.diffTable!.classList.add('selected-left');
+ element.diffTable!.classList.remove('selected-right');
+
+ const selection = document.getSelection();
+ if (selection === null) assert.fail('no selection');
+ selection.removeAllRanges();
+ const range = document.createRange();
+ range.setStart(diffTable.querySelector('div.contentText')!.firstChild!, 3);
+ range.setEnd(
+ diffTable.querySelectorAll('div.contentText')[4]!.firstChild!,
+ 2
+ );
+ selection.addRange(range);
+ assert.equal(element.getSelectedText(Side.LEFT, false), 'ba\nzin\nga');
+ });
+
+ test('copies comments', () => {
+ element.diffTable!.classList.add('selected-left');
+ element.diffTable!.classList.add('selected-comment');
+ element.diffTable!.classList.remove('selected-right');
+ const selection = document.getSelection();
+ if (selection === null) assert.fail('no selection');
+ selection.removeAllRanges();
+ const range = document.createRange();
+ range.setStart(
+ diffTable.querySelector('.gr-formatted-text *')!.firstChild!,
+ 3
+ );
+ range.setEnd(
+ diffTable.querySelectorAll('.gr-formatted-text *')[2].childNodes[2],
+ 7
+ );
+ selection.addRange(range);
+ assert.equal(
+ 's is a comment\nThis is a differ',
+ element.getSelectedText(Side.LEFT, true)
+ );
+ });
+
+ test('respects astral chars in comments', () => {
+ element.diffTable!.classList.add('selected-left');
+ element.diffTable!.classList.add('selected-comment');
+ element.diffTable!.classList.remove('selected-right');
+ const selection = document.getSelection();
+ if (selection === null) assert.fail('no selection');
+ selection.removeAllRanges();
+ const range = document.createRange();
+ const nodes = diffTable.querySelectorAll('.gr-formatted-text *');
+ range.setStart(nodes[2].childNodes[2], 13);
+ range.setEnd(nodes[2].childNodes[2], 23);
+ selection.addRange(range);
+ assert.equal('mment 💩 u', element.getSelectedText(Side.LEFT, true));
+ });
+
+ test('defers to default behavior for textarea', () => {
+ element.diffTable!.classList.add('selected-left');
+ element.diffTable!.classList.remove('selected-right');
+ const selectedTextSpy = sinon.spy(element, 'getSelectedText');
+ emulateCopyOn(diffTable.querySelector('textarea'));
+ assert.isFalse(selectedTextSpy.called);
+ });
+
+ test('regression test for 4794', () => {
+ element.diffTable!.classList.add('selected-right');
+ element.diffTable!.classList.remove('selected-left');
+
+ const selection = document.getSelection();
+ if (!selection) assert.fail('no selection');
+ selection.removeAllRanges();
+ const range = document.createRange();
+ range.setStart(
+ diffTable.querySelectorAll('div.contentText')[1]!.firstChild!,
+ 4
+ );
+ range.setEnd(
+ diffTable.querySelectorAll('div.contentText')[1]!.firstChild!,
+ 10
+ );
+ selection.addRange(range);
+ assert.equal(element.getSelectedText(Side.RIGHT, false), ' other');
+ });
+
+ test('copies to end of side (issue 7895)', () => {
+ element.diffTable!.classList.add('selected-left');
+ element.diffTable!.classList.remove('selected-right');
+ const selection = document.getSelection();
+ if (selection === null) assert.fail('no selection');
+ selection.removeAllRanges();
+ const range = document.createRange();
+ range.setStart(diffTable.querySelector('div.contentText')!.firstChild!, 3);
+ range.setEnd(
+ diffTable.querySelectorAll('div.contentText')[4]!.firstChild!,
+ 2
+ );
+ selection.addRange(range);
+ assert.equal(element.getSelectedText(Side.LEFT, false), 'ba\nzin\nga');
+ });
+
+ suite('getTextContentForRange', () => {
+ let selection: Selection;
+ let range: Range;
+ let nodes: NodeListOf<GrFormattedText>;
+
+ setup(() => {
+ element.diffTable!.classList.add('selected-left');
+ element.diffTable!.classList.add('selected-comment');
+ element.diffTable!.classList.remove('selected-right');
+ const s = document.getSelection();
+ if (s === null) assert.fail('no selection');
+ selection = s;
+ selection.removeAllRanges();
+ range = document.createRange();
+ nodes = diffTable.querySelectorAll('.gr-formatted-text *');
+ });
+
+ test('multi level element contained in range', () => {
+ range.setStart(nodes[2].childNodes[0], 1);
+ range.setEnd(nodes[2].childNodes[2], 7);
+ selection.addRange(range);
+ assert.equal(
+ element.getTextContentForRange(diffTable, selection, range),
+ 'his is a differ'
+ );
+ });
+
+ test('multi level element as startContainer of range', () => {
+ range.setStart(nodes[2].childNodes[1], 0);
+ range.setEnd(nodes[2].childNodes[2], 7);
+ selection.addRange(range);
+ assert.equal(
+ element.getTextContentForRange(diffTable, selection, range),
+ 'a differ'
+ );
+ });
+
+ test('startContainer === endContainer', () => {
+ range.setStart(nodes[0].firstChild!, 2);
+ range.setEnd(nodes[0].firstChild!, 12);
+ selection.addRange(range);
+ assert.equal(
+ element.getTextContentForRange(diffTable, selection, range),
+ 'is is a co'
+ );
+ });
+ });
+});
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 a38ec91..34c2a33 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
@@ -80,6 +80,7 @@
import {isSafari, toggleClass} from '../../../utils/dom-util';
import {assertIsDefined} from '../../../utils/common-util';
import {debounce, DelayedTask} from '../../../utils/async-util';
+import {GrDiffSelection} from '../gr-diff-selection/gr-diff-selection';
const NO_NEWLINE_LEFT = 'No newline at end of left file.';
const NO_NEWLINE_RIGHT = 'No newline at end of right file.';
@@ -99,7 +100,6 @@
export interface GrDiff {
$: {
- highlights: GrDiffHighlight;
diffBuilder: GrDiffBuilderElement;
diffTable: HTMLTableElement;
};
@@ -294,6 +294,10 @@
private renderDiffTableTask?: DelayedTask;
+ private diffSelection = new GrDiffSelection();
+
+ private highlights = new GrDiffHighlight();
+
constructor() {
super();
this._setLoading(true);
@@ -315,6 +319,8 @@
this.renderDiffTableTask?.cancel();
this._unobserveIncrementalNodes();
this._unobserveNodes();
+ this.diffSelection.cleanup();
+ this.highlights.cleanup();
super.disconnectedCallback();
}
@@ -357,7 +363,7 @@
// and pass the shadow DOM selection into gr-diff-highlight, where the
// corresponding range is determined and normalized.
const selection = this._getShadowOrDocumentSelection();
- this.$.highlights.handleSelectionChange(selection, false);
+ this.highlights.handleSelectionChange(selection, false);
};
private readonly handleMouseUp = () => {
@@ -365,7 +371,7 @@
// mouse-up if there's a selection that just covers a line change. We
// can't do that on selection change since the user may still be dragging.
const selection = this._getShadowOrDocumentSelection();
- this.$.highlights.handleSelectionChange(selection, true);
+ this.highlights.handleSelectionChange(selection, true);
};
/** Gets the current selection, preferring the shadow DOM selection. */
@@ -404,7 +410,7 @@
const range = getRange(threadEl);
if (!range) return undefined;
- return {side, range, hovering: false, rootId: threadEl.rootId};
+ return {side, range, rootId: threadEl.rootId};
}
// TODO(brohlfs): Rewrite `.map().filter() as ...` with `.reduce()` instead.
@@ -430,7 +436,6 @@
this.push('_commentRanges', {
side: Side.RIGHT,
range: this.highlightRange,
- hovering: true,
rootId: '',
});
}
@@ -498,7 +503,7 @@
}
isRangeSelected() {
- return !!this.$.highlights.selectedRange;
+ return !!this.highlights.selectedRange;
}
toggleLeftDiff() {
@@ -590,7 +595,7 @@
if (!this.isRangeSelected()) {
throw Error('Selection is needed for new range comment');
}
- const selectedRange = this.$.highlights.selectedRange;
+ const selectedRange = this.highlights.selectedRange;
if (!selectedRange) throw Error('selected range not set');
const {side, range} = selectedRange;
this._createCommentForSelection(side, range);
@@ -813,6 +818,10 @@
this._diffLength = this.getDiffLength(newValue);
this._debounceRenderDiffTable();
}
+ if (this.diff) {
+ this.diffSelection.init(this.diff, this.$.diffTable);
+ this.highlights.init(this.$.diffTable, this.$.diffBuilder);
+ }
}
/**
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 e05e85a..6d36b89 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
@@ -487,6 +487,10 @@
color: var(--link-color);
padding: var(--spacing-m) 0 var(--spacing-m) 48px;
}
+ #diffTable {
+ /* for gr-selection-action-box positioning */
+ position: relative;
+ }
#diffTable:focus {
outline: none;
}
@@ -670,6 +674,14 @@
.token-highlight {
background-color: var(--token-highlighting-color, #fffd54);
}
+
+ gr-selection-action-box {
+ /**
+ * Needs z-index to appear above wrapped content, since it's inserted
+ * into DOM before it.
+ */
+ z-index: 10;
+ }
</style>
<style include="gr-syntax-theme">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
@@ -686,44 +698,36 @@
class$="[[_computeContainerClass(loggedIn, viewMode, displayLine)]]"
on-click="_handleTap"
>
- <gr-diff-selection diff="[[diff]]">
- <gr-diff-highlight
- id="highlights"
- logged-in="[[loggedIn]]"
- comment-ranges="{{_commentRanges}}"
- >
- <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>
+ <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>
- <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>
- </gr-diff-highlight>
- </gr-diff-selection>
+ <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>
</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.js
index 714005e..c8d8a2f 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.js
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.js
@@ -14,9 +14,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
import '../../../test/common-test-setup-karma.js';
-import {getMockDiffResponse} from '../../../test/mocks/diff-response.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';
@@ -51,21 +50,21 @@
setup(() => {
element = basicFixture.instantiate();
- sinon.stub(element.$.highlights, 'handleSelectionChange');
+ sinon.stub(element.highlights, 'handleSelectionChange');
});
test('enabled if logged in', async () => {
element.loggedIn = true;
emulateSelection();
await flush();
- assert.isTrue(element.$.highlights.handleSelectionChange.called);
+ assert.isTrue(element.highlights.handleSelectionChange.called);
});
test('ignored if logged out', async () => {
element.loggedIn = false;
emulateSelection();
await flush();
- assert.isFalse(element.$.highlights.handleSelectionChange.called);
+ assert.isFalse(element.highlights.handleSelectionChange.called);
});
});
@@ -191,7 +190,7 @@
element.changeNum = 123;
element.patchRange = {basePatchNum: 1, patchNum: 2};
element.path = 'file.txt';
- element.$.diffBuilder.diff = getMockDiffResponse();
+ element.$.diffBuilder.diff = createDiff();
element.$.diffBuilder.prefs = {...MINIMAL_PREFS};
element.$.diffBuilder._builder = element.$.diffBuilder._getDiffBuilder();
@@ -526,7 +525,7 @@
suite('getCursorStops', () => {
function setupDiff() {
- element.diff = getMockDiffResponse();
+ element.diff = createDiff();
element.prefs = {
context: 10,
tab_size: 8,
@@ -807,7 +806,7 @@
return Promise.resolve({});
});
sinon.stub(element, 'getDiffLength').returns(10000);
- element.diff = getMockDiffResponse();
+ element.diff = createDiff();
element.noRenderOnPrefsChange = true;
});
@@ -1217,7 +1216,7 @@
});
test('getDiffLength', () => {
- const diff = getMockDiffResponse();
+ const diff = createDiff();
assert.equal(element.getDiffLength(diff), 52);
});
diff --git a/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.ts b/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.ts
index 6c8a5e9..70cec64 100644
--- a/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.ts
+++ b/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.ts
@@ -20,7 +20,6 @@
export interface CommentRangeLayer {
side: Side;
range: CommentRange;
- hovering: boolean;
// New drafts don't have a rootId.
rootId?: string;
}
@@ -40,7 +39,6 @@
* highlights.
*/
interface CommentRangeLineLayer {
- hovering: boolean;
longRange: boolean;
id: string;
// start char (0-based)
@@ -59,7 +57,7 @@
const RANGE_BASE_ONLY = 'style-scope gr-diff range';
const RANGE_HIGHLIGHT = 'style-scope gr-diff range rangeHighlight';
-const HOVER_HIGHLIGHT = 'style-scope gr-diff range rangeHoverHighlight';
+// Note that there is also `rangeHoverHighlight` being set by GrDiffHighlight.
export class GrRangedCommentLayer implements DiffLayer {
private knownRanges: CommentRangeLayer[] = [];
@@ -95,11 +93,8 @@
el,
range.start,
range.end - range.start,
- (range.hovering
- ? HOVER_HIGHLIGHT
- : range.longRange
- ? RANGE_BASE_ONLY
- : RANGE_HIGHLIGHT) + ` ${strToClassName(range.id)}`
+ (range.longRange ? RANGE_BASE_ONLY : RANGE_HIGHLIGHT) +
+ ` ${strToClassName(range.id)}`
);
}
}
@@ -139,17 +134,15 @@
}
private addRange(commentRange: CommentRangeLayer) {
- const {side, range, hovering} = commentRange;
+ const {side, range} = commentRange;
const longRange = isLongCommentRange(range);
this.updateRangesMap({
side,
range,
- hovering,
- operation: (forLine, startChar, endChar, hovering) => {
+ operation: (forLine, startChar, endChar) => {
forLine.push({
start: startChar,
end: endChar,
- hovering,
id: id(commentRange),
longRange,
});
@@ -158,11 +151,10 @@
}
private removeRange(commentRange: CommentRangeLayer) {
- const {side, range, hovering} = commentRange;
+ const {side, range} = commentRange;
this.updateRangesMap({
side,
range,
- hovering,
operation: forLine => {
const index = forLine.findIndex(
lineRange => id(commentRange) === lineRange.id
@@ -175,21 +167,19 @@
private updateRangesMap(options: {
side: Side;
range: CommentRange;
- hovering: boolean;
operation: (
forLine: CommentRangeLineLayer[],
start: number,
- end: number,
- hovering: boolean
+ end: number
) => void;
}) {
- const {side, range, hovering, operation} = options;
+ const {side, range, operation} = options;
const forSide = this.rangesMap[side] || (this.rangesMap[side] = {});
for (let line = range.start_line; line <= range.end_line; line++) {
const forLine = forSide[line] || (forSide[line] = []);
const start = line === range.start_line ? range.start_character : 0;
const end = line === range.end_line ? range.end_character : -1;
- operation(forLine, start, end, hovering);
+ operation(forLine, start, end);
}
this.notifyUpdateRange(range.start_line, range.end_line, side);
}
@@ -199,25 +189,20 @@
const lineNum = side === Side.LEFT ? line.beforeNumber : line.afterNumber;
if (lineNum === 'FILE' || lineNum === 'LOST') return [];
const ranges: CommentRangeLineLayer[] = this.rangesMap[side][lineNum] || [];
- return (
- ranges
- .map(range => {
- // Make a copy, so that the normalization below does not mess with
- // our map.
- range = {...range};
- range.end = range.end === -1 ? line.text.length : range.end;
+ return ranges.map(range => {
+ // Make a copy, so that the normalization below does not mess with
+ // our map.
+ range = {...range};
+ range.end = range.end === -1 ? line.text.length : range.end;
- // Normalize invalid ranges where the start is after the end but the
- // start still makes sense. Set the end to the end of the line.
- // @see Issue 5744
- if (range.start >= range.end && range.start < line.text.length) {
- range.end = line.text.length;
- }
+ // Normalize invalid ranges where the start is after the end but the
+ // start still makes sense. Set the end to the end of the line.
+ // @see Issue 5744
+ if (range.start >= range.end && range.start < line.text.length) {
+ range.end = line.text.length;
+ }
- return range;
- })
- // Sort the ranges so that hovering highlights are on top.
- .sort((a, b) => (a.hovering && !b.hovering ? 1 : 0))
- );
+ return range;
+ });
}
}
diff --git a/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.ts b/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.ts
index 4e35645..15d14e3 100644
--- a/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.ts
@@ -24,7 +24,6 @@
start_line: 36,
},
rootId: 'a',
- hovering: false,
};
const rangeB: CommentRangeLayer = {
@@ -36,7 +35,6 @@
start_line: 10,
},
rootId: 'b',
- hovering: false,
};
const rangeC: CommentRangeLayer = {
@@ -47,7 +45,6 @@
start_character: 5,
start_line: 100,
},
- hovering: false,
};
const rangeD: CommentRangeLayer = {
@@ -59,7 +56,6 @@
start_line: 55,
},
rootId: 'd',
- hovering: false,
};
const rangeE: CommentRangeLayer = {
@@ -70,7 +66,6 @@
start_character: 1,
start_line: 60,
},
- hovering: false,
};
suite('gr-ranged-comment-layer', () => {
diff --git a/polygerrit-ui/app/test/mocks/diff-response.ts b/polygerrit-ui/app/test/mocks/diff-response.ts
deleted file mode 100644
index 46d30b6..0000000
--- a/polygerrit-ui/app/test/mocks/diff-response.ts
+++ /dev/null
@@ -1,137 +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 {DiffInfo} from '../../types/diff';
-
-export function getMockDiffResponse(): DiffInfo {
- // Return new response, so tests can't affect each other - if a test somehow
- // modifies it, the future calls return original value
- // Do not put it to a const outside of a method
- return {
- meta_a: {
- name: 'lorem-ipsum.txt',
- content_type: 'text/plain',
- lines: 45,
- },
- meta_b: {
- name: 'lorem-ipsum.txt',
- content_type: 'text/plain',
- lines: 48,
- },
- 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: [
- 'Lorem ipsum dolor sit amet, suspendisse inceptos vehicula, ' +
- 'nulla phasellus.',
- 'Mattis lectus.',
- 'Sodales duis.',
- 'Orci a faucibus.',
- ],
- },
- {
- b: [
- 'Nullam neque, ligula ac, id blandit.',
- 'Sagittis tincidunt torquent, tempor nunc amet.',
- 'At rhoncus id.',
- ],
- },
- {
- ab: [
- 'Sem nascetur, erat ut, non in.',
- 'A donec, venenatis pellentesque dis.',
- 'Mauris mauris.',
- 'Quisque nisl duis, facilisis viverra.',
- 'Justo purus, semper eget et.',
- ],
- },
- {
- a: [
- 'Est amet, vestibulum pellentesque.',
- 'Erat ligula.',
- 'Justo eros.',
- 'Fringilla quisque.',
- ],
- },
- {
- ab: [
- 'Arcu eget, rhoncus amet cursus, ipsum elementum.',
- 'Eros suspendisse.',
- ],
- },
- {
- a: ['Rhoncus tempor, ultricies aliquam ipsum.'],
- b: ['Rhoncus tempor, ultricies praesent ipsum.'],
- edit_a: [[26, 7]],
- edit_b: [[26, 8]],
- },
- {
- ab: [
- 'Sollicitudin duis.',
- 'Blandit blandit, ante nisl fusce.',
- 'Felis ac at, tellus consectetuer.',
- 'Sociis ligula sapien, egestas leo.',
- 'Cum pulvinar, sed mauris, cursus neque velit.',
- 'Augue porta lobortis.',
- 'Nibh lorem, amet fermentum turpis, vel pulvinar diam.',
- 'Id quam ipsum, id urna et, massa suspendisse.',
- 'Ac nec, nibh praesent.',
- 'Rutrum vestibulum.',
- 'Est tellus, bibendum habitasse.',
- 'Justo facilisis, vel nulla.',
- 'Donec eu, vulputate neque aliquam, nulla dui.',
- 'Risus adipiscing in.',
- 'Lacus arcu arcu.',
- 'Urna velit.',
- 'Urna a dolor.',
- 'Lectus magna augue, convallis mattis tortor, sed tellus ' +
- 'consequat.',
- 'Etiam dui, blandit wisi.',
- 'Mi nec.',
- 'Vitae eget vestibulum.',
- 'Ullamcorper nunc ante, nec imperdiet felis, consectetur in.',
- 'Ac eget.',
- 'Vel fringilla, interdum pellentesque placerat, proin ante.',
- ],
- },
- {
- b: [
- 'Eu congue risus.',
- 'Enim ac, quis elementum.',
- 'Non et elit.',
- 'Etiam aliquam, diam vel nunc.',
- ],
- },
- {
- ab: [
- 'Nec at.',
- 'Arcu mauris, venenatis lacus fermentum, praesent duis.',
- 'Pellentesque amet et, tellus duis.',
- 'Ipsum arcu vitae, justo elit, sed libero tellus.',
- 'Metus rutrum euismod, vivamus sodales, vel arcu nisl.',
- ],
- },
- ],
- };
-}
diff --git a/polygerrit-ui/app/test/test-data-generators.ts b/polygerrit-ui/app/test/test-data-generators.ts
index 04b6c93..2fffc9a 100644
--- a/polygerrit-ui/app/test/test-data-generators.ts
+++ b/polygerrit-ui/app/test/test-data-generators.ts
@@ -33,6 +33,7 @@
CommentInfo,
CommentLinkInfo,
CommentLinks,
+ CommentRange,
CommitId,
CommitInfo,
ConfigInfo,
@@ -122,6 +123,7 @@
} from '../api/rest-api';
import {RunResult} from '../models/checks/checks-model';
import {Category, RunStatus} from '../api/checks';
+import {DiffInfo} from '../api/diff';
const TEST_DEFAULT_EXPRESSION = 'label:Verified=MAX -label:Verified=MIN';
export const TEST_PROJECT_NAME: RepoName = 'test-project' as RepoName;
@@ -467,6 +469,122 @@
};
}
+export function createDiff(): DiffInfo {
+ return {
+ meta_a: {
+ name: 'lorem-ipsum.txt',
+ content_type: 'text/plain',
+ lines: 45,
+ },
+ meta_b: {
+ name: 'lorem-ipsum.txt',
+ content_type: 'text/plain',
+ lines: 48,
+ },
+ 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: [
+ 'Lorem ipsum dolor sit amet, suspendisse inceptos vehicula, ' +
+ 'nulla phasellus.',
+ 'Mattis lectus.',
+ 'Sodales duis.',
+ 'Orci a faucibus.',
+ ],
+ },
+ {
+ b: [
+ 'Nullam neque, ligula ac, id blandit.',
+ 'Sagittis tincidunt torquent, tempor nunc amet.',
+ 'At rhoncus id.',
+ ],
+ },
+ {
+ ab: [
+ 'Sem nascetur, erat ut, non in.',
+ 'A donec, venenatis pellentesque dis.',
+ 'Mauris mauris.',
+ 'Quisque nisl duis, facilisis viverra.',
+ 'Justo purus, semper eget et.',
+ ],
+ },
+ {
+ a: [
+ 'Est amet, vestibulum pellentesque.',
+ 'Erat ligula.',
+ 'Justo eros.',
+ 'Fringilla quisque.',
+ ],
+ },
+ {
+ ab: [
+ 'Arcu eget, rhoncus amet cursus, ipsum elementum.',
+ 'Eros suspendisse.',
+ ],
+ },
+ {
+ a: ['Rhoncus tempor, ultricies aliquam ipsum.'],
+ b: ['Rhoncus tempor, ultricies praesent ipsum.'],
+ edit_a: [[26, 7]],
+ edit_b: [[26, 8]],
+ },
+ {
+ ab: [
+ 'Sollicitudin duis.',
+ 'Blandit blandit, ante nisl fusce.',
+ 'Felis ac at, tellus consectetuer.',
+ 'Sociis ligula sapien, egestas leo.',
+ 'Cum pulvinar, sed mauris, cursus neque velit.',
+ 'Augue porta lobortis.',
+ 'Nibh lorem, amet fermentum turpis, vel pulvinar diam.',
+ 'Id quam ipsum, id urna et, massa suspendisse.',
+ 'Ac nec, nibh praesent.',
+ 'Rutrum vestibulum.',
+ 'Est tellus, bibendum habitasse.',
+ 'Justo facilisis, vel nulla.',
+ 'Donec eu, vulputate neque aliquam, nulla dui.',
+ 'Risus adipiscing in.',
+ 'Lacus arcu arcu.',
+ 'Urna velit.',
+ 'Urna a dolor.',
+ 'Lectus magna augue, convallis mattis tortor, sed tellus ' +
+ 'consequat.',
+ 'Etiam dui, blandit wisi.',
+ 'Mi nec.',
+ 'Vitae eget vestibulum.',
+ 'Ullamcorper nunc ante, nec imperdiet felis, consectetur in.',
+ 'Ac eget.',
+ 'Vel fringilla, interdum pellentesque placerat, proin ante.',
+ ],
+ },
+ {
+ b: [
+ 'Eu congue risus.',
+ 'Enim ac, quis elementum.',
+ 'Non et elit.',
+ 'Etiam aliquam, diam vel nunc.',
+ ],
+ },
+ {
+ ab: [
+ 'Nec at.',
+ 'Arcu mauris, venenatis lacus fermentum, praesent duis.',
+ 'Pellentesque amet et, tellus duis.',
+ 'Ipsum arcu vitae, justo elit, sed libero tellus.',
+ 'Metus rutrum euismod, vivamus sodales, vel arcu nisl.',
+ ],
+ },
+ ],
+ };
+}
+
export function createMergeable(): MergeableInfo {
return {
submit_type: SubmitType.MERGE_IF_NECESSARY,
@@ -535,6 +653,15 @@
};
}
+export function createRange(): CommentRange {
+ return {
+ start_line: 1,
+ start_character: 0,
+ end_line: 1,
+ end_character: 1,
+ };
+}
+
export function createComment(
extra: Partial<CommentInfo | DraftInfo> = {}
): CommentInfo {
diff --git a/polygerrit-ui/app/test/test-utils.ts b/polygerrit-ui/app/test/test-utils.ts
index 0c63de0..985bec1 100644
--- a/polygerrit-ui/app/test/test-utils.ts
+++ b/polygerrit-ui/app/test/test-utils.ts
@@ -296,6 +296,19 @@
element.dispatchEvent(new KeyboardEvent('keydown', eventOptions));
}
+export function mouseDown(element: HTMLElement) {
+ const rect = element.getBoundingClientRect();
+ const eventOptions = {
+ bubbles: true,
+ composed: true,
+ clientX: (rect.left + rect.right) / 2,
+ clientY: (rect.top + rect.bottom) / 2,
+ screenX: (rect.left + rect.right) / 2,
+ screenY: (rect.top + rect.bottom) / 2,
+ };
+ element.dispatchEvent(new MouseEvent('mousedown', eventOptions));
+}
+
export function assertFails(promise: Promise<unknown>, error?: unknown) {
promise
.then((_v: unknown) => {